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 tofigma.*global. No DOM, nowindow, nofetch, noXMLHttpRequest. UsesetTimeoutbut notsetInterval. - •UI (
ui.html): Runs in an<iframe>. Full browser APIs including DOM,fetch, Canvas, Web Workers. Communicates with sandbox viapostMessage.
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"], or["figma", "figjam"] - •
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') { /* handle */ }
};
Listening (UI):
window.onmessage = (event) => {
const msg = event.data.pluginMessage;
if (!msg) return;
if (msg.type === 'results') { /* handle */ }
};
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, // Inject Figma's CSS variables for theme support
visible: true, // Set false for headless plugins
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
// All instances on current page
const instances = figma.currentPage.findAllWithCriteria({ types: ['INSTANCE'] });
// Find by name
const node = figma.currentPage.findOne(n => n.name === 'Button');
// Find all text nodes
const texts = figma.currentPage.findAll(n => n.type === 'TEXT');
// Walk children manually
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 benullif 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
// On a ComponentSetNode:
const variantGroupProperties = componentSet.variantGroupProperties;
// { "Size": { values: ["S", "M", "L"] }, "State": { values: ["Default", "Hover"] } }
// On an InstanceNode — change variant:
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); // First 5 chars at 24px textNode.setRangeFills(0, 5, [solidPaint]); // First 5 chars colored
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
// Solid color (RGB 0–1, NOT 0–255)
const solidFill = { type: 'SOLID', color: { r: 1, g: 0.4, b: 0.3 }, opacity: 1 };
// Gradient
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]],
};
// Image fill
const imageHash = figma.createImage(imageBytes).hash;
const imageFill = { type: 'IMAGE', scaleMode: 'FILL', imageHash };
// Apply fills (REPLACES all fills — clone first to preserve existing)
node.fills = [solidFill];
CRITICAL: Colors use 0–1 range. Convert from hex: r = 0xE5 / 255.
Auto Layout
const frame = figma.createFrame(); frame.layoutMode = 'VERTICAL'; // or 'HORIZONTAL' frame.primaryAxisAlignItems = 'CENTER'; // main axis: MIN | CENTER | MAX | SPACE_BETWEEN frame.counterAxisAlignItems = 'CENTER'; // cross axis: MIN | CENTER | MAX frame.itemSpacing = 12; // gap between children frame.paddingTop = 16; frame.paddingBottom = 16; frame.paddingLeft = 16; frame.paddingRight = 16; frame.primaryAxisSizingMode = 'AUTO'; // AUTO (hug) or FIXED frame.counterAxisSizingMode = 'AUTO';
Children in auto-layout use layoutAlign and layoutGrow:
child.layoutAlign = 'STRETCH'; // fill container width (in vertical layout) child.layoutGrow = 1; // expand to fill remaining space
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
// Per-plugin persistent storage (survives plugin restarts)
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 }); // Returns handle
// handle.cancel(); to dismiss
Selection & Viewport
// Read selection
const selected = figma.currentPage.selection; // ReadonlyArray<SceneNode>
// Set selection
figma.currentPage.selection = [node1, node2];
// Scroll to nodes
figma.viewport.scrollAndZoomIntoView([node]);
// Current viewport
const { x, y, width, height } = figma.viewport.bounds;
const zoom = figma.viewport.zoom;
Best Practices
Performance
- •Batch reads before writes — Figma flushes layout after writes; interleaving is expensive
- •Use
findAllWithCriteriaoverfindAllwith 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)
Error Handling
- •Wrap all
awaitcalls in try/catch — nodes can be deleted, components missing, fonts unavailable - •
getMainComponentAsync()can returnnullfor deleted/inaccessible components - •
importComponentByKeyAsyncthrows if the library isn't published or access is missing - •Always verify
node.parentexists before manipulating tree position
UI Development
- •Use
themeColors: trueand 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
onmessagedefensively — always checkmsg.type
Plugin Structure Pattern
// code.js — clean message router 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:
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
// Get all local variables
const variables = await figma.variables.getLocalVariablesAsync();
// Create a variable collection
const collection = figma.variables.createVariableCollection('Colors');
// Create a variable
const variable = figma.variables.createVariable('primary', collection, 'COLOR');
variable.setValueForMode(collection.modes[0].modeId, { r: 0.2, g: 0.4, b: 1 });
// Bind a variable to a node property
node.setBoundVariable('fills', 0, variable);
Additional Resources
- •For detailed Plugin API reference, see plugin-api-reference.md
- •For REST API reference, see rest-api-reference.md
- •For code examples and patterns, see examples.md