| name | figma-plugin-dev |
| description | Expert Figma plugin developer with deep knowledge of the Plugin API, REST API, sandbox architecture, Grid/Auto Layout systems, Variables/theming, and UI best practices. Use when building, debugging, or extending Figma plugins or widgets, working with manifest.json, code.js/code.ts, ui.html, or any figma.* API code. Covers Figma, FigJam, Slides, and Buzz editor types. |
Figma Plugin Development
You are an expert Figma plugin developer. Apply this knowledge whenever building, modifying, or debugging Figma plugins.
Plugin Architecture
Figma plugins run in a dual-context architecture:
┌─────────────────────────────┐
│ UI Thread (ui.html) │ ← iframe, full browser APIs, DOM, fetch
│ postMessage ↕ │
│ Sandbox (code.js) │ ← No DOM, limited JS, access to figma.*
└─────────────────────────────┘
- Sandbox (
code.js/code.ts): Runs in a minimal JS environment. Has access to figma.* global. No DOM, no window, no fetch, no XMLHttpRequest. Use setTimeout but not setInterval.
- UI (
ui.html): Runs in an <iframe>. Full browser APIs including DOM, fetch, Canvas, Web Workers. Communicates with sandbox via postMessage.
manifest.json
{
"name": "Plugin Name",
"id": "unique-id",
"api": "1.0.0",
"main": "code.js",
"ui": "ui.html",
"editorType": ["figma"],
"capabilities": [],
"enableProposedApi": false,
"networkAccess": {
"allowedDomains": ["api.figma.com"],
"reasoning": "Why network access is needed"
}
}
Key fields:
editorType: ["figma"], ["figjam"], ["slides"], ["buzz"], or combinations
networkAccess.allowedDomains: Required for any network calls from UI. Use ["none"] if no network needed, ["*"] for unrestricted
capabilities: ["inspect"] for Dev Mode plugins, ["codegen"] for code generators
enableProposedApi: Enable experimental APIs (may break)
Message Passing
Sandbox → UI:
figma.ui.postMessage({ type: 'results', data: payload });
UI → Sandbox:
parent.postMessage({ pluginMessage: { type: 'action', data: payload } }, '*');
Listening (sandbox):
figma.ui.onmessage = async (msg) => {
if (msg.type === 'action') { }
};
Listening (UI):
window.onmessage = (event) => {
const msg = event.data.pluginMessage;
if (!msg) return;
if (msg.type === 'results') { }
};
CRITICAL: Messages are serialized via structured clone. You cannot send functions, DOM nodes, class instances, or circular references. Plain objects, arrays, strings, numbers, booleans, null, Uint8Array, and ArrayBuffer are safe.
Showing the UI
figma.showUI(__html__, {
width: 400,
height: 500,
themeColors: true,
visible: true,
position: { x: 0, y: 0 },
});
Resize at runtime: figma.ui.resize(newWidth, newHeight)
Node Tree & Types
Figma's document is a tree:
Document
└─ Page (PageNode)
├─ Frame (FrameNode)
│ ├─ Rectangle (RectangleNode)
│ ├─ Text (TextNode)
│ └─ Instance (InstanceNode)
├─ Component (ComponentNode)
├─ ComponentSet (ComponentSetNode) ← variant container
├─ Group (GroupNode)
├─ Section (SectionNode)
├─ Vector / Star / Ellipse / Polygon / Line / BooleanOperation
└─ Slice / Connector / Stamp / Widget
Key Node Properties (shared)
| Property | Type | Notes |
|---|
id | string | Unique within file |
name | string | Layer name |
type | string | e.g. 'FRAME', 'TEXT' |
parent | BaseNode | null | Parent node |
children | ReadonlyArray<SceneNode> | Only on container nodes |
visible | boolean | Visibility |
locked | boolean | Lock state |
opacity | number | 0–1 |
x, y | number | Position relative to parent |
width, height | number | Dimensions |
rotation | number | Degrees |
fills | ReadonlyArray<Paint> | Fill paints |
strokes | ReadonlyArray<Paint> | Stroke paints |
effects | ReadonlyArray<Effect> | Drop shadow, blur, etc. |
constraints | Constraints | Horizontal/vertical constraints |
layoutMode | 'NONE' | 'HORIZONTAL' | 'VERTICAL' | Auto-layout direction |
Traversal
const instances = figma.currentPage.findAllWithCriteria({ types: ['INSTANCE'] });
const node = figma.currentPage.findOne(n => n.name === 'Button');
const texts = figma.currentPage.findAll(n => n.type === 'TEXT');
function walk(node, callback) {
callback(node);
if ('children' in node) {
for (const child of node.children) walk(child, callback);
}
}
findAllWithCriteria is faster than findAll with a filter — prefer it when filtering by type.
Components & Instances
ComponentSetNode (variant group, e.g. "Button")
├─ ComponentNode (variant: "State=Default, Size=M")
├─ ComponentNode (variant: "State=Hover, Size=M")
└─ ComponentNode (variant: "State=Default, Size=L")
component.key — globally unique key (persists across files when published)
instance.mainComponent — resolves the source ComponentNode (may be null if missing)
await instance.getMainComponentAsync() — preferred async version, resolves remote components
instance.swapComponent(newComponent) — swap to a different component
await figma.importComponentByKeyAsync(key) — import from a library by key
await figma.importComponentSetByKeyAsync(key) — import variant set by key
component.createInstance() — create a new instance of the component
Variant Properties
const variantGroupProperties = componentSet.variantGroupProperties;
instance.setProperties({ "Size": "L", "State": "Hover" });
Text
Always load fonts before modifying text:
const textNode = figma.createText();
await figma.loadFontAsync({ family: "Inter", style: "Regular" });
textNode.characters = "Hello World";
textNode.fontSize = 16;
textNode.fills = [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }];
For mixed-style text, load all needed fonts first, then use range methods:
textNode.setRangeFontSize(0, 5, 24);
textNode.setRangeFills(0, 5, [solidPaint]);
GOTCHA: textNode.fontName throws if the text has mixed fonts. Use textNode.getRangeFontName(0, 1) for safety, or load all fonts via textNode.getRangeAllFontNames(0, textNode.characters.length).
Styles & Paint
const solidFill = { type: 'SOLID', color: { r: 1, g: 0.4, b: 0.3 }, opacity: 1 };
const gradientFill = {
type: 'GRADIENT_LINEAR',
gradientStops: [
{ position: 0, color: { r: 1, g: 0, b: 0, a: 1 } },
{ position: 1, color: { r: 0, g: 0, b: 1, a: 1 } },
],
gradientTransform: [[1, 0, 0], [0, 1, 0]],
};
const imageHash = figma.createImage(imageBytes).hash;
const imageFill = { type: 'IMAGE', scaleMode: 'FILL', imageHash };
node.fills = [solidFill];
CRITICAL: Colors use 0–1 range. Convert from hex: r = 0xE5 / 255.
Auto Layout
const frame = figma.createFrame();
frame.layoutMode = 'VERTICAL';
frame.primaryAxisAlignItems = 'CENTER';
frame.counterAxisAlignItems = 'CENTER';
frame.itemSpacing = 12;
frame.paddingTop = 16;
frame.paddingBottom = 16;
frame.paddingLeft = 16;
frame.paddingRight = 16;
frame.primaryAxisSizingMode = 'AUTO';
frame.counterAxisSizingMode = 'AUTO';
Children in auto-layout use layoutAlign and layoutGrow:
child.layoutAlign = 'STRETCH';
child.layoutGrow = 1;
Grid Layout (CSS Grid)
Figma supports CSS Grid-style layouts on frames (added 2025):
const grid = figma.createFrame();
grid.layoutMode = 'GRID';
grid.gridRowCount = 3;
grid.gridColumnCount = 4;
grid.gridRowGap = 8;
grid.gridColumnGap = 8;
grid.gridRowSizes = [{ type: 'FIXED', value: 100 }, { type: 'FLEX', value: 1 }];
grid.gridColumnSizes = [{ type: 'FIXED', value: 200 }, { type: 'FLEX', value: 2 }];
child.gridRowSpan = 2;
child.gridColumnSpan = 1;
child.gridRowAnchorIndex = 0;
child.gridColumnAnchorIndex = 1;
child.gridChildHorizontalAlign = 'CENTER';
child.gridChildVerticalAlign = 'CENTER';
child.setGridChildPosition(row, column);
grid.appendChildAt(node, row, column);
Export
const pngBytes = await node.exportAsync({ format: 'PNG', constraint: { type: 'SCALE', value: 2 } });
const svgString = await node.exportAsync({ format: 'SVG' });
const jpgBytes = await node.exportAsync({ format: 'JPG', constraint: { type: 'HEIGHT', value: 120 } });
const pdfBytes = await node.exportAsync({ format: 'PDF' });
Storage & Persistence
await figma.clientStorage.setAsync('key', value);
const value = await figma.clientStorage.getAsync('key');
await figma.clientStorage.deleteAsync('key');
const keys = await figma.clientStorage.keysAsync();
Values must be JSON-serializable. Size limit: ~1MB per key.
Notifications
figma.notify('Success message');
figma.notify('Error occurred', { error: true });
figma.notify('Processing...', { timeout: Infinity });
Selection & Viewport
const selected = figma.currentPage.selection;
figma.currentPage.selection = [node1, node2];
figma.viewport.scrollAndZoomIntoView([node]);
const { x, y, width, height } = figma.viewport.bounds;
const zoom = figma.viewport.zoom;
Deprecations & Breaking Changes
InstanceNode.resetOverrides() — DEPRECATED (Update 120, Nov 2025). Use instance.removeOverrides() instead
- GridTrackSize setter — type only auto-sets to
'FIXED' from 'HUG' now (Update 120)
- REST API:
GET /v2/teams/:team_id/webhooks — DEPRECATED. Use GET /v2/webhooks?context=team&context_id=:team_id
- REST API: PATs — Maximum expiration reduced to 90 days (non-expiring PATs prohibited, Apr 2025)
- REST API:
files:read scope — Deprecated. Use specific scopes: file_content:read, file_comments:read, etc.
- REST API: OAuth refresh — Use
POST /v1/oauth/token (not the old /v1/oauth/refresh endpoint)
Best Practices
Performance
- Batch reads before writes — Figma flushes layout after writes; interleaving is expensive
- Use
findAllWithCriteria over findAll with callback for type-based filtering
- Limit
exportAsync — expensive; generate thumbnails at small sizes (e.g. HEIGHT: 120)
- Avoid deep recursion on large files — use iterative traversal or limit scope to selection/page
- Send progress messages to UI during long operations (e.g. every 10 nodes)
- Check
node.removed before operating on nodes — they may have been deleted between async calls
Error Handling
- Wrap all
await calls in try/catch — nodes can be deleted, components missing, fonts unavailable
getMainComponentAsync() can return null for deleted/inaccessible components
importComponentByKeyAsync throws if the library isn't published or access is missing
- Always verify
node.parent exists before manipulating tree position
- Check for
429 Too Many Requests on REST API calls and implement exponential backoff
Security
- Never hardcode tokens — store PATs using
figma.clientStorage and pass to UI via postMessage
- PATs expire in ≤90 days — implement token refresh flows; non-expiring PATs are no longer allowed
- Use scoped permissions — prefer
file_content:read over the deprecated files:read
- Restrict
networkAccess.allowedDomains — use specific domains, not ["*"], unless necessary
UI Development
- Use
themeColors: true and Figma's CSS variables for light/dark theme support
- Keep the UI single-file (
ui.html) with inline CSS/JS for simplicity, or use a bundler for complex UIs
- Design for the compact plugin panel — typical width 300–520px
- Use Figma's design system tokens:
--figma-color-bg, --figma-color-text, --figma-color-border
- Handle
onmessage defensively — always check msg.type
- Use
figma.ui.getPosition() to read the current UI panel position
Plugin Structure Pattern
figma.showUI(__html__, { width: 400, height: 500, themeColors: true });
figma.ui.onmessage = async (msg) => {
switch (msg.type) {
case 'action-a': await handleA(msg); break;
case 'action-b': await handleB(msg); break;
case 'close': figma.closePlugin(); break;
}
};
TypeScript
Use Figma's type definitions for full IntelliSense (now ships with docstrings as of May 2025):
npm install --save-dev @figma/plugin-typings
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"strict": true,
"typeRoots": ["./node_modules/@figma/plugin-typings"]
}
}
For UI code, create a separate tsconfig.ui.json with "lib": ["ES2020", "DOM"].
Variables & Styles API
const variables = await figma.variables.getLocalVariablesAsync();
const collection = figma.variables.createVariableCollection('Colors');
const variable = figma.variables.createVariable('primary', collection, 'COLOR');
variable.setValueForMode(collection.modes[0].modeId, { r: 0.2, g: 0.4, b: 1 });
node.setBoundVariable('fills', 0, variable);
Extended Variable Collections (Theming)
Extended collections enable theme overrides on top of library collections (added Nov 2025):
const extended = await figma.variables.extendLibraryCollectionByKeyAsync(collectionKey, 'My Theme');
variable.setValueForMode(modeId, newValue);
const overrides = extended.variableOverrides;
variable.removeOverrideForMode(modeId);
extended.removeOverridesForVariable(variable);
const values = await variable.valuesByModeForCollectionAsync(collection);
const rootId = extended.rootVariableCollectionId;
Additional Resources