Figma Plugin Development
Architecture Overview
Figma plugins consist of three parts:
- manifest.json — Configuration, permissions, and capabilities
- Main code (code.ts) — Runs in QuickJS sandbox with
figmaAPI access - UI (optional) — HTML/CSS/JS iframe with browser APIs
┌─────────────────────────────────────────────────────────┐
│ MAIN THREAD (Sandbox) │ UI IFRAME (Browser) │
│ ✓ figma global API │ ✓ DOM, fetch, Canvas │
│ ✓ Document manipulation │ ✓ React/Vue/Svelte │
│ ✓ Native fetch API │ ✓ WebSockets │
│ ✗ No DOM access │ ✗ No figma API │
│ └────── postMessage ──────┘ │
└─────────────────────────────────────────────────────────┘
Important: The sandbox now has a native Fetch API — network requests no longer require the UI iframe.
Quick Start
Minimal manifest.json
{
"name": "My Plugin",
"id": "000000000000000000",
"api": "1.0.0",
"main": "code.js",
"editorType": ["figma"],
"documentAccess": "dynamic-page"
}
Minimal Plugin (No UI)
const rect = figma.createRectangle();
rect.fills = [figma.util.solidPaint('#FF5500')];
figma.currentPage.appendChild(rect);
figma.closePlugin('Created rectangle');
Plugin with UI
// code.ts
figma.showUI(__html__, { width: 300, height: 200, themeColors: true });
figma.ui.onmessage = async (msg) => {
if (msg.type === 'create-rect') {
const rect = figma.createRectangle();
rect.resize(msg.width, msg.height);
figma.currentPage.appendChild(rect);
}
figma.closePlugin();
};
<!-- ui.html -->
<button id="create">Create</button>
<script>
document.getElementById('create').onclick = () => {
parent.postMessage({ pluginMessage: { type: 'create-rect', width: 100, height: 100 } }, '*');
};
</script>
Manifest Configuration
Required Fields
| Field | Description |
|---|---|
name |
Plugin name in Figma menu |
id |
Unique ID (assigned by Figma on publish) |
api |
API version, always "1.0.0" |
main |
Path to compiled JavaScript entry |
editorType |
Array: "figma", "figjam", "dev", "slides", "buzz" |
documentAccess |
Always "dynamic-page" — mandatory for all new plugins |
Editor Types
Five editor types with distinct API surfaces:
"figma"— Full design editor (default)"figjam"— Whiteboard (stickies, connectors, tables, stamps, timer)"dev"— Dev Mode (read-only, codegen, inspect, dev resources)"slides"— Figma Slides (SlideNode, SlideRowNode, transitions)"buzz"— Figma Buzz (marketing assets, smart resize)
Invalid combinations: ["figjam", "dev"] and ["slides", "dev"].
Permissions
{ "permissions": ["currentuser", "activeusers", "fileusers", "teamlibrary", "payments"] }
Capabilities
{ "capabilities": ["codegen", "inspect", "textreview", "vscode"] }
codegen— Code generation in Dev Mode (requirescodegenLanguages)inspect— Custom inspect panel in Dev Modetextreview— Text review/linting (firestextreviewevent)vscode— VS Code integration
Network Access
{
"networkAccess": {
"allowedDomains": ["api.example.com", "*.example.com"],
"reasoning": "Required for fetching data from our API",
"devAllowedDomains": ["http://localhost:3000"]
}
}
Supports wildcards, WebSocket schemes (ws://, wss://), ["none"], or ["*"] (unrestricted, requires reasoning). Plugin iframes have a null origin — CORS with Access-Control-Allow-Origin: * is required on target servers.
Menu Commands and Mode
{
"menu": [
{ "name": "Create Shape", "command": "create" },
{ "separator": true },
{ "name": "Settings", "command": "settings" }
]
}
figma.on('run', ({ command }) => {
if (command === 'settings') figma.showUI(__uiFiles__['settings']);
});
figma.mode // 'default' | 'textreview' | 'inspect' | 'codegen' | 'linkpreview' | 'auth'
Project Setup
TypeScript Configuration
npm init -y
npm install --save-dev typescript @figma/plugin-typings esbuild
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020", "lib": ["ES2020"], "strict": true,
"typeRoots": ["./node_modules/@types", "./node_modules/@figma"],
"outDir": "./dist"
},
"include": ["src/**/*.ts"]
}
Current @figma/plugin-typings: 1.121.0. Use eslint-plugin-figma-plugins to catch deprecated sync API calls.
Build with esbuild
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/code.ts'], bundle: true, outfile: 'dist/code.js',
target: 'es2020', minify: process.argv.includes('--minify'),
});
Essential Patterns
Selection Handling
const selection = figma.currentPage.selection;
if (selection.length === 0) {
figma.notify('Select at least one layer', { error: true });
figma.closePlugin();
return;
}
for (const node of selection) {
if (node.type === 'TEXT') { /* process */ }
}
Node Creation
const rect = figma.createRectangle();
const frame = figma.createFrame();
const text = figma.createText();
const component = figma.createComponent();
const section = figma.createSection();
const slide = figma.createSlide(); // Slides
const slideRow = figma.createSlideRow(); // Slides
node.x = 100; node.y = 200;
node.resize(300, 200);
frame.appendChild(rect);
Colors and Fills
RGB values are 0-1, not 0-255.
const solidPaint = figma.util.solidPaint('#FF5500');
rect.fills = [{ type: 'SOLID', color: { r: 1, g: 0.5, b: 0 }, opacity: 0.8 }];
// CRITICAL: Clone before modifying — fills/strokes/effects are read-only
const fills = JSON.parse(JSON.stringify(rect.fills));
fills[0].color.r = 0.5;
rect.fills = fills;
Text (MUST Load Fonts)
const text = figma.createText();
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
text.characters = 'Hello World';
text.fontSize = 24;
text.fontName = { family: 'Inter', style: 'Bold' };
text.setRangeFontSize(0, 5, 32);
text.setRangeFills(0, 5, [figma.util.solidPaint('#FF0000')]);
text.setRangeHyperlink(0, 5, { type: 'URL', value: 'https://example.com' });
if (text.fontName === figma.mixed) {
const fonts = text.getRangeAllFontNames(0, text.characters.length);
await Promise.all(fonts.map(f => figma.loadFontAsync(f)));
}
Auto Layout
const frame = figma.createFrame();
frame.layoutMode = 'HORIZONTAL'; // 'VERTICAL' | 'NONE'
frame.primaryAxisSizingMode = 'AUTO';
frame.counterAxisSizingMode = 'AUTO';
frame.itemSpacing = 16;
frame.paddingTop = frame.paddingBottom = 20;
frame.paddingLeft = frame.paddingRight = 16;
frame.primaryAxisAlignItems = 'CENTER';
frame.counterAxisAlignItems = 'CENTER';
frame.layoutWrap = 'WRAP';
frame.counterAxisSpacing = 12;
child.layoutSizingHorizontal = 'FILL'; // 'FIXED' | 'HUG' | 'FILL'
Variables (Design Tokens)
Variables are the API for design tokens. See references/variables-api.md for the full reference.
const collection = figma.variables.createVariableCollection('Colors');
const primaryColor = figma.variables.createVariable('primary', collection, 'COLOR');
primaryColor.setValueForMode(collection.defaultModeId, { r: 0.2, g: 0.4, b: 1 });
rect.setBoundVariable('fills', primaryColor); // 30+ bindable fields
rect.setBoundVariable('cornerRadius', radiusVar);
rect.setBoundVariable('itemSpacing', spacingVar);
const allColors = await figma.variables.getLocalVariablesAsync('COLOR');
const libVar = await figma.variables.importVariableByKeyAsync(key);
Annotations
node.annotations = [{
label: 'Ready for development',
properties: [{ type: 'fills' }, { type: 'width' }, { type: 'fontSize' }]
}];
const categories = await figma.annotations.getAnnotationCategoriesAsync();
Components and Instances
const component = figma.createComponent();
component.addComponentProperty("Label", "TEXT", "Button");
component.addComponentProperty("Disabled", "BOOLEAN", false);
component.addComponentProperty("Icon", "INSTANCE_SWAP", iconId, {
preferredValues: [{ type: 'COMPONENT', key: starIcon.key }]
});
const instance = component.createInstance();
instance.setProperties({ 'Label#0:1': 'Submit' });
const main = await instance.getMainComponentAsync(); // ASYNC required
Data Storage
node.setPluginData('key', JSON.stringify({ value: 1 }));
const data = JSON.parse(node.getPluginData('key') || '{}');
await figma.clientStorage.setAsync('prefs', { theme: 'dark' });
const prefs = await figma.clientStorage.getAsync('prefs');
Images
const image = await figma.createImageAsync('https://example.com/image.png');
rect.fills = [{ type: 'IMAGE', imageHash: image.hash, scaleMode: 'FILL' }];
const bytes = await node.exportAsync({ format: 'PNG', constraint: { type: 'SCALE', value: 2 } });
Payments (Monetization)
Requires "payments" in manifest permissions.
const { status } = figma.payments;
if (status.type === 'UNPAID') {
await figma.payments.initiateCheckoutAsync({ interstitial: 'TRIAL_ENDED' });
} else if (status.type === 'PAID') { /* Full access */ }
const secondsSinceFirstRun = figma.payments.getUserFirstRanSecondsAgo();
UI Communication
figma.ui.postMessage({ type: 'data', items: [1, 2, 3] }); // code → UI
parent.postMessage({ pluginMessage: { type: 'action', data: 123 } }, '*'); // UI → code
Theme CSS Variables
With themeColors: true, detect dark mode via .figma-dark on <body>:
body { background: var(--figma-color-bg); color: var(--figma-color-text); }
.button { background: var(--figma-color-bg-brand); color: var(--figma-color-text-onbrand); }
Plugin UI Libraries
@create-figma-plugin/ui(v4+) — Best option. Preact components matching Figma UI3.figma-plugin-ds— Vanilla CSS/JS, older UI2 style.- Figma's UI3 Kit is a design file only (no code components).
Events
figma.on('run', ({ command, parameters }) => { });
figma.on('selectionchange', () => { });
figma.on('currentpagechange', () => { });
figma.on('documentchange', (event) => { /* event.documentChanges */ });
figma.on('drop', (event) => { return false; });
figma.on('stylechange', (event) => { });
figma.on('close', () => { /* SYNC ONLY */ });
figma.currentPage.on('nodechange', ({ nodeChanges }) => { }); // page-level
Performance
figma.skipInvisibleInstanceChildren = true; // ALWAYS enable
const texts = figma.currentPage.findAllWithCriteria({ types: ['TEXT'] });
const tagged = figma.currentPage.findAllWithCriteria({ pluginData: { keys: ['myKey'] } });
await page.loadAsync(); // load only needed pages
Critical Rules
- Always call
figma.closePlugin()— Plugin runs forever otherwise - Use
documentAccess: "dynamic-page"— Mandatory for all new plugins - Use async API methods — Sync versions are deprecated. Use
Asyncvariants. - Load fonts before text changes — Throws without loaded font
- Clone arrays before modifying — fills, effects, strokes are read-only
- No async in
closeevent — Must be synchronous - Declare network domains — CSP blocks undeclared domains
- RGB values are 0-1 — Not 0-255
- UI messages need
pluginMessagewrapper —{ pluginMessage: data } - Dev Mode plugins are read-only — Cannot create or modify nodes
- Codegen
generatehas 15s timeout — NoshowUI()inside it
References
- Full API Reference: See references/api-reference.md for node types (35), methods, properties, text API, components
- Variables API: See references/variables-api.md for design tokens — collections, modes, binding, scopes
- Editors & Codegen: See references/editors-codegen.md for FigJam, Slides, Buzz, Dev Mode, codegen, dev resources, annotations
- Publishing: See references/publishing.md for review, payments setup, distribution
Debugging
console.log('Debug:', value); // Plugins > Development > Open Console
figma.notify('Status message');
figma.notify('Error', { error: true, timeout: 5000 });