Figma Plugins
Building Figma plugins using the Plugin API to automate tasks, generate assets, and extend Figma's capabilities
You are an expert in building Figma plugins for developer-designer collaboration using Figma. ## Key Points - **Keep the main thread fast** — Heavy computation or large node traversals block Figma's UI. Batch operations and yield periodically with `setTimeout`. - **Use `loadFontAsync` before modifying text** — Figma requires fonts to be loaded before you can change `characters` on a text node. Always await the font load. - **Validate selection before acting** — Check `figma.currentPage.selection` length and node types. Provide clear `figma.notify()` messages when selection is invalid. - **Bundle for production** — Use esbuild or webpack to bundle TypeScript into a single `code.js` and inline the UI into `ui.html` for distribution. - **Store user preferences** — Use `figma.clientStorage.setAsync()` and `getAsync()` to persist settings between plugin runs. - **Forgetting the sandbox boundary** — The main thread cannot use `fetch`, `document`, or `window`. These must be handled in the UI iframe and communicated via messages. - **Not loading fonts** — Attempting to set `textNode.characters` without first calling `loadFontAsync` throws an error. - **Modifying detached nodes** — If a node is removed from the document while your plugin holds a reference, operations on it will fail. Always verify `node.removed === false`. - **Large payloads in postMessage** — Sending the entire document tree between threads is slow. Serialize only the data you need. - **Skipping error handling on async operations** — Font loading, client storage, and any UI-proxied fetch calls can fail. Wrap them in try/catch and notify the user on failure. ## Quick Example ``` ┌─────────────────────────┐ postMessage ┌─────────────────┐ │ Main Thread (sandbox) │ ◄──────────────────► │ UI (iframe) │ │ figma.* API access │ │ DOM, fetch, │ │ No DOM │ │ No figma.* │ └─────────────────────────┘ └─────────────────┘ ``` ```bash # Create a new plugin project mkdir my-figma-plugin && cd my-figma-plugin npm init -y npm install --save-dev typescript @figma/plugin-typings ```
skilldb get figma-development-skills/Figma PluginsFull skill: 248 linesBuilding Figma Plugins — Figma for Developers
You are an expert in building Figma plugins for developer-designer collaboration using Figma.
Core Philosophy
Overview
Figma plugins extend the editor's functionality by running custom code within Figma's sandbox. Plugins can read and modify the document tree, create UI panels, interact with external APIs, and automate repetitive design tasks. They are built with TypeScript/JavaScript and optionally an HTML/CSS UI rendered in an iframe. The plugin ecosystem supports both private (organization-only) and public (community) distribution.
Core Concepts
Plugin Architecture
A Figma plugin has two execution contexts:
- Main thread (sandbox) — Runs in a JavaScript sandbox with access to the Figma Plugin API (
figma.*). This is where you read/write the document, create nodes, and manipulate properties. It has no DOM access. - UI thread (iframe) — An optional HTML page rendered in an
<iframe>. It has full DOM/browser APIs but no direct Figma API access. Communication between the two threads happens via message passing.
┌─────────────────────────┐ postMessage ┌─────────────────┐
│ Main Thread (sandbox) │ ◄──────────────────► │ UI (iframe) │
│ figma.* API access │ │ DOM, fetch, │
│ No DOM │ │ No figma.* │
└─────────────────────────┘ └─────────────────┘
Manifest
Every plugin requires a manifest.json:
{
"name": "My Plugin",
"id": "1234567890",
"api": "1.0.0",
"main": "dist/code.js",
"ui": "dist/ui.html",
"editorType": ["figma"],
"permissions": ["currentuser"]
}
Project Setup
# Create a new plugin project
mkdir my-figma-plugin && cd my-figma-plugin
npm init -y
npm install --save-dev typescript @figma/plugin-typings
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"typeRoots": ["./node_modules/@figma/plugin-typings"]
}
}
Implementation Patterns
Minimal Plugin — Rename Selected Layers
// code.ts (main thread)
const selection = figma.currentPage.selection;
if (selection.length === 0) {
figma.notify('Please select at least one layer.');
figma.closePlugin();
} else {
let count = 0;
for (const node of selection) {
if (node.type === 'TEXT') {
node.name = `Text/${node.characters.slice(0, 20)}`;
count++;
} else {
node.name = `${node.type.toLowerCase()}/${node.name}`;
count++;
}
}
figma.notify(`Renamed ${count} layers.`);
figma.closePlugin();
}
Plugin with UI — Color Palette Generator
// code.ts (main thread)
figma.showUI(__html__, { width: 320, height: 400 });
figma.ui.onmessage = async (msg) => {
if (msg.type === 'generate-palette') {
const { baseColor, steps } = msg;
const frame = figma.createFrame();
frame.name = 'Color Palette';
frame.layoutMode = 'HORIZONTAL';
frame.itemSpacing = 8;
frame.paddingLeft = 16;
frame.paddingRight = 16;
frame.paddingTop = 16;
frame.paddingBottom = 16;
for (let i = 0; i < steps; i++) {
const rect = figma.createRectangle();
rect.resize(64, 64);
rect.cornerRadius = 8;
const lightness = 0.1 + (i / (steps - 1)) * 0.8;
rect.fills = [{
type: 'SOLID',
color: { r: lightness, g: lightness * 0.8, b: lightness * 1.2 }
}];
frame.appendChild(rect);
}
figma.currentPage.appendChild(frame);
figma.viewport.scrollAndZoomIntoView([frame]);
figma.notify(`Created palette with ${steps} swatches.`);
}
if (msg.type === 'cancel') {
figma.closePlugin();
}
};
<!-- ui.html -->
<div id="app">
<h3>Color Palette Generator</h3>
<label>Base Color: <input type="color" id="baseColor" value="#6C5CE7"></label>
<label>Steps: <input type="number" id="steps" value="7" min="3" max="15"></label>
<button id="generate">Generate</button>
<button id="cancel">Cancel</button>
</div>
<script>
document.getElementById('generate').onclick = () => {
parent.postMessage({
pluginMessage: {
type: 'generate-palette',
baseColor: document.getElementById('baseColor').value,
steps: parseInt(document.getElementById('steps').value),
}
}, '*');
};
document.getElementById('cancel').onclick = () => {
parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*');
};
</script>
Reading Component Properties
function inspectComponent(node: ComponentNode | InstanceNode) {
const properties: Record<string, any> = {};
if (node.type === 'COMPONENT') {
for (const prop of node.componentPropertyDefinitions
? Object.entries(node.componentPropertyDefinitions)
: []) {
const [key, def] = prop;
properties[key] = {
type: def.type,
defaultValue: def.defaultValue,
variantOptions: def.type === 'VARIANT' ? def.variantOptions : undefined,
};
}
}
return properties;
}
Calling External APIs from the UI
Since the main thread sandbox has no fetch, external API calls go through the UI:
// code.ts
figma.showUI(__html__, { visible: false });
figma.ui.postMessage({ type: 'fetch-data', url: 'https://api.example.com/data' });
figma.ui.onmessage = (msg) => {
if (msg.type === 'fetch-response') {
// Use msg.data to populate the design
const textNode = figma.createText();
figma.loadFontAsync({ family: 'Inter', style: 'Regular' }).then(() => {
textNode.characters = JSON.stringify(msg.data, null, 2);
figma.currentPage.appendChild(textNode);
figma.closePlugin();
});
}
};
<!-- ui.html (hidden) -->
<script>
onmessage = async (event) => {
const msg = event.data.pluginMessage;
if (msg.type === 'fetch-data') {
const res = await fetch(msg.url);
const data = await res.json();
parent.postMessage({ pluginMessage: { type: 'fetch-response', data } }, '*');
}
};
</script>
Best Practices
- Keep the main thread fast — Heavy computation or large node traversals block Figma's UI. Batch operations and yield periodically with
setTimeout. - Use
loadFontAsyncbefore modifying text — Figma requires fonts to be loaded before you can changecharacterson a text node. Always await the font load. - Validate selection before acting — Check
figma.currentPage.selectionlength and node types. Provide clearfigma.notify()messages when selection is invalid. - Bundle for production — Use esbuild or webpack to bundle TypeScript into a single
code.jsand inline the UI intoui.htmlfor distribution. - Store user preferences — Use
figma.clientStorage.setAsync()andgetAsync()to persist settings between plugin runs.
Common Pitfalls
- Forgetting the sandbox boundary — The main thread cannot use
fetch,document, orwindow. These must be handled in the UI iframe and communicated via messages. - Not loading fonts — Attempting to set
textNode.characterswithout first callingloadFontAsyncthrows an error. - Modifying detached nodes — If a node is removed from the document while your plugin holds a reference, operations on it will fail. Always verify
node.removed === false. - Large payloads in postMessage — Sending the entire document tree between threads is slow. Serialize only the data you need.
- Skipping error handling on async operations — Font loading, client storage, and any UI-proxied fetch calls can fail. Wrap them in try/catch and notify the user on failure.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add figma-development-skills
Related Skills
Auto Layout
Understanding Figma auto layout and translating it directly to CSS Flexbox and Grid implementations
Component Inspection
Inspecting Figma components and systematically translating their structure, variants, and properties into production code
Design Tokens Export
Exporting design tokens from Figma into platform-agnostic formats for use in codebases and design systems
Dev Mode
Using Figma Dev Mode to inspect designs, extract code snippets, and streamline the design-to-code workflow
Figma API
Using the Figma REST API to programmatically access files, components, images, and design data
Adversarial Code Review
Adversarial implementation review methodology that validates code completeness against requirements with fresh objectivity. Uses a coach-player dialectical loop to catch real gaps in security, logic, and data flow.