figma-plugins

Build Figma plugins with full Plugin API access. Use when creating plugins for Figma, FigJam, Slides, or Dev Mode that manipulate designs, generate content, export assets, manage styles/variables, wor

SKILL.md

GitHub

Figma Plugin Development

Architecture Overview

Figma plugins consist of three parts:

  1. manifest.json — Configuration, permissions, and capabilities
  2. Main code (code.ts) — Runs in QuickJS sandbox with figma API access
  3. 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 (requires codegenLanguages)
  • inspect — Custom inspect panel in Dev Mode
  • textreview — Text review/linting (fires textreview event)
  • 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

  1. Always call figma.closePlugin() — Plugin runs forever otherwise
  2. Use documentAccess: "dynamic-page" — Mandatory for all new plugins
  3. Use async API methods — Sync versions are deprecated. Use Async variants.
  4. Load fonts before text changes — Throws without loaded font
  5. Clone arrays before modifying — fills, effects, strokes are read-only
  6. No async in close event — Must be synchronous
  7. Declare network domains — CSP blocks undeclared domains
  8. RGB values are 0-1 — Not 0-255
  9. UI messages need pluginMessage wrapper — { pluginMessage: data }
  10. Dev Mode plugins are read-only — Cannot create or modify nodes
  11. Codegen generate has 15s timeout — No showUI() inside it

References

Debugging

console.log('Debug:', value);  // Plugins > Development > Open Console
figma.notify('Status message');
figma.notify('Error', { error: true, timeout: 5000 });