
Figma Use
- 1.8k installs
- 24.5k repo stars
- Updated July 14, 2026
- openai/skills
figma-use is an agent skill that **mandatory prerequisite** — you must invoke this skill before every `use_figma` tool call. never call `use_figma` directly without loading this skill first. skipping it causes common, ha
About
figma-use is an agent skill from openai/skills that **mandatory prerequisite** — you must invoke this skill before every `use_figma` tool call. never call `use_figma` directly without loading this skill first. skipping it causes common, hard-to-debug f. # use_figma — Figma Plugin API Skill Use `use_figma` MCP to execute JavaScript in Figma files via the Plugin API. All detailed reference docs live in `references/`. **Always pass `skillNames: "figma-use"` when calling `use_figma`.** This is a logging parameter used to track skill usage — it does not affect execution. **If the task involves build Developers invoke figma-use during build/integrations work for ai & agent building tasks. The skill documents triggers, prerequisites, and step-by-step workflows grounded in SKILL.md. Compatible with Claude Code, Cursor, and Codex agent runtimes that load marketplace skills.
- use_figma — Figma Plugin API Skill
- Use `use_figma` MCP to execute JavaScript in Figma files via the Plugin API. All detailed reference docs live in `refere
- Always pass `skillNames: "figma-use"` when calling `use_figma`.** This is a logging parameter used to track skill usage
- 3. `figma.notify()` **throws "not implemented"** — never use it
- 4. `console.log()` is NOT returned — use `return` for output
Figma Use by the numbers
- 1,765 all-time installs (skills.sh)
- +151 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #720 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
figma-use capabilities & compatibility
- Capabilities
- use_figma — figma plugin api skill · use `use_figma` mcp to execute javascript in fig · always pass `skillnames: "figma use"` when calli · 3. `figma.notify()` **throws "not implemented"** · 4. `console.log()` is not returned — use `return
- Use cases
- orchestration
What figma-use says it does
Use `use_figma` MCP to execute JavaScript in Figma files via the Plugin API. All detailed reference docs live in `references/`.
**Always pass `skillNames: "figma-use"` when calling `use_figma`.** This is a logging parameter used to track skill usage — it does not affect execution.
2. **Write plain JavaScript with top-level `await` and `return`.** Code is automatically wrapped in an async context. Do NOT wrap in `(async () => { ... })()`.
npx skills add https://github.com/openai/skills --skill figma-useAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.8k |
|---|---|
| repo stars | ★ 24.5k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 14, 2026 |
| Repository | openai/skills ↗ |
What it does
**MANDATORY prerequisite** — you MUST invoke this skill BEFORE every `use_figma` tool call. NEVER call `use_figma` directly without loading this skill first. Skipping it causes common, hard-to-debug f
Who is it for?
Developers working on ai & agent building during build tasks.
Skip if: Tasks outside AI & Agent Building scope described in SKILL.md.
When should I use this skill?
**MANDATORY prerequisite** — you MUST invoke this skill BEFORE every `use_figma` tool call. NEVER call `use_figma` directly without loading this skill first. Skipping it causes common, hard-to-debug f
What you get
Completed ai & agent building workflow aligned with SKILL.md steps.
- mutated Figma nodes
- bound design variables
- component variants
Files
use_figma — Figma Plugin API Skill
Use use_figma MCP to execute JavaScript in Figma files via the Plugin API. All detailed reference docs live in references/.
Always pass `skillNames: "figma-use"` when calling `use_figma`. This is a logging parameter used to track skill usage — it does not affect execution.
If the task involves building or updating a full page, screen, or multi-section layout in Figma from code, also load figma-generate-design. It provides the workflow for discovering design system components via search_design_system, importing them, and assembling screens incrementally. Both skills work together: this one for the API rules, that one for the screen-building workflow.
Before anything, load plugin-api-standalone.index.md to understand what is possible. When you are asked to write plugin API code, use this context to grep plugin-api-standalone.d.ts for relevant types, methods, and properties. This is the definitive source of truth for the API surface. It is a large typings file, so do not load it all at once, grep for relevant sections as needed.
IMPORTANT: Whenever you work with design systems, start with working-with-design-systems/wwds.md to understand the key concepts, processes, and guidelines for working with design systems in Figma. Then load the more specific references for components, variables, text styles, and effect styles as needed.
1. Critical Rules
1. Use `return` to send data back. The return value is JSON-serialized automatically (objects, arrays, strings, numbers). Do NOT call figma.closePlugin() or wrap code in an async IIFE — this is handled for you. 2. Write plain JavaScript with top-level `await` and `return`. Code is automatically wrapped in an async context. Do NOT wrap in (async () => { ... })(). 3. figma.notify() throws "not implemented" — never use it 3a. getPluginData() / setPluginData() are not supported in use_figma — do not use them. Use getSharedPluginData() / setSharedPluginData() instead (these ARE supported), or track node IDs by returning them and passing them to subsequent calls. 4. console.log() is NOT returned — use return for output 5. Work incrementally in small steps. Break large operations into multiple use_figma calls. Validate after each step. This is the single most important practice for avoiding bugs. 6. Colors are 0–1 range (not 0–255): {r: 1, g: 0, b: 0} = red 7. Fills/strokes are read-only arrays — clone, modify, reassign 8. Font MUST be loaded before any text operation: await figma.loadFontAsync({family, style}) 9. Pages load incrementally — use await figma.setCurrentPageAsync(page) to switch pages and load their content (see Page Rules below) 10. setBoundVariableForPaint returns a NEW paint — must capture and reassign 11. createVariable accepts collection object or ID string (object preferred) 12. `layoutSizingHorizontal/Vertical = 'FILL'` MUST be set AFTER `parent.appendChild(child)` — setting before append throws. Same applies to 'HUG' on non-auto-layout nodes. 13. Position new top-level nodes away from (0,0). Nodes appended directly to the page default to (0,0). Scan figma.currentPage.children to find a clear position (e.g., to the right of the rightmost node). This only applies to page-level nodes — nodes nested inside other frames or auto-layout containers are positioned by their parent. See Gotchas. 14. On `use_figma` error, STOP. Do NOT immediately retry. Failed scripts are atomic — if a script errors, it is not executed at all and no changes are made to the file. Read the error message carefully, fix the script, then retry. See Error Recovery. 15. MUST `return` ALL created/mutated node IDs. Whenever a script creates new nodes or mutates existing ones on the canvas, collect every affected node ID and return them in a structured object (e.g. return { createdNodeIds: [...], mutatedNodeIds: [...] }). This is essential for subsequent calls to reference, validate, or clean up those nodes. 16. Always set `variable.scopes` explicitly when creating variables. The default ALL_SCOPES pollutes every property picker — almost never what you want. Use specific scopes like ["FRAME_FILL", "SHAPE_FILL"] for backgrounds, ["TEXT_FILL"] for text colors, ["GAP"] for spacing, etc. See variable-patterns.md for the full list. 17. `await` every Promise. Never leave a Promise unawaited — unawaited async calls (e.g. figma.loadFontAsync(...) without await, or figma.setCurrentPageAsync(page) without await) will fire-and-forget, causing silent failures or race conditions. The script may return before the async operation completes, leading to missing data or half-applied changes.
For detailed WRONG/CORRECT examples of each rule, see Gotchas & Common Mistakes.
2. Page Rules (Critical)
Page context resets between `use_figma` calls — figma.currentPage starts on the first page each time.
Switching pages
Use await figma.setCurrentPageAsync(page) to switch pages and load their content. The sync setter figma.currentPage = page throws an error in use_figma runtimes.
// Switch to a specific page (loads its content)
const targetPage = figma.root.children.find((p) => p.name === "My Page");
await figma.setCurrentPageAsync(targetPage);
// targetPage.children is now populated
// Iterate over all pages
for (const page of figma.root.children) {
await figma.setCurrentPageAsync(page);
// page.children is now loaded — read or modify them here
}Across script runs
figma.currentPage resets to the first page at the start of each use_figma call. If your workflow spans multiple calls and targets a non-default page, call await figma.setCurrentPageAsync(page) at the start of each invocation.
You can call use_figma multiple times to incrementally build on the file state, or to retrieve information before writing another script. For example, write a script to get metadata about existing nodes, return that data, then use it in a subsequent script to modify those nodes.
3. return Is Your Output Channel
The agent sees ONLY the value you return. Everything else is invisible.
- Returning IDs (CRITICAL): Every script that creates or mutates canvas nodes MUST return all affected node IDs — e.g.
return { createdNodeIds: [...], mutatedNodeIds: [...] }. This is a hard requirement, not optional. - Progress reporting:
return { createdNodeIds: [...], count: 5, errors: [] } - Error info: Thrown errors are automatically captured and returned — just let them propagate or
throwexplicitly. console.log()output is never returned to the agent- Always return actionable data (IDs, counts, status) so subsequent calls can reference created objects
4. Editor Mode
use_figma works in design mode (editorType "figma", the default). FigJam ("figjam") has a different set of available node types — most design nodes are blocked there.
Available in design mode: Rectangle, Frame, Component, Text, Ellipse, Star, Line, Vector, Polygon, BooleanOperation, Slice, Page, Section, TextPath.
Blocked in design mode: Sticky, Connector, ShapeWithText, CodeBlock, Slide, SlideRow, Webpage.
5. Incremental Workflow (How to Avoid Bugs)
The most common cause of bugs is trying to do too much in a single use_figma call. Work in small steps and validate after each one.
The pattern
1. Inspect first. Before creating anything, run a read-only use_figma to discover what already exists in the file — pages, components, variables, naming conventions. Match what's there. 2. Do one thing per call. Create variables in one call, create components in the next, compose layouts in another. Don't try to build an entire screen in one script. 3. Return IDs from every call. Always return created node IDs, variable IDs, collection IDs as objects (e.g. return { createdNodeIds: [...] }). You'll need these as inputs to subsequent calls. 4. Validate after each step. Use get_metadata to verify structure (counts, names, hierarchy, positions). Use get_screenshot after major milestones to catch visual issues. 5. Fix before moving on. If validation reveals a problem, fix it before proceeding to the next step. Don't build on a broken foundation.
Suggested step order for complex tasks
Step 1: Inspect file — discover existing pages, components, variables, conventions
Step 2: Create tokens/variables (if needed)
→ validate with get_metadata
Step 3: Create individual components
→ validate with get_metadata + get_screenshot
Step 4: Compose layouts from component instances
→ validate with get_screenshot
Step 5: Final verificationWhat to validate at each step
| After... | Check with get_metadata | Check with get_screenshot |
|---|---|---|
| Creating variables | Collection count, variable count, mode names | — |
| Creating components | Child count, variant names, property definitions | Variants visible, not collapsed, grid readable |
| Binding variables | Node properties reflect bindings | Colors/tokens resolved correctly |
| Composing layouts | Instance nodes have mainComponent, hierarchy correct | No cropped/clipped text, no overlapping elements, correct spacing |
6. Error Recovery & Self-Correction
`use_figma` is atomic — failed scripts do not execute. If a script errors, no changes are made to the file. The file remains in the same state as before the call. This means there are no partial nodes, no orphaned elements from the failed script, and retrying after a fix is safe.
When use_figma returns an error
1. STOP. Do not immediately fix the code and retry. 2. Read the error message carefully. Understand exactly what went wrong — wrong API usage, missing font, invalid property value, etc. 3. If the error is unclear, call get_metadata or get_screenshot to understand the current file state. 4. Fix the script based on the error message. 5. Retry the corrected script.
Common self-correction patterns
| Error message | Likely cause | How to fix |
|---|---|---|
"not implemented" | Used figma.notify() | Remove it — use return for output |
"node must be an auto-layout frame..." | Set FILL/HUG before appending to auto-layout parent | Move appendChild before layoutSizingX = 'FILL' |
"Setting figma.currentPage is not supported" | Used sync page setter | Use await figma.setCurrentPageAsync(page) |
| Property value out of range | Color channel > 1 (used 0–255 instead of 0–1) | Divide by 255 |
"Cannot read properties of null" | Node doesn't exist (wrong ID, wrong page) | Check page context, verify ID |
| Script hangs / no response | Infinite loop or unresolved promise | Check for while(true) or missing await; ensure code terminates |
"The node with id X does not exist" | Parent instance was implicitly detached by a child detachInstance(), changing IDs | Re-discover nodes by traversal from a stable (non-instance) parent frame |
When the script succeeds but the result looks wrong
1. Call get_metadata to check structural correctness (hierarchy, counts, positions). 2. Call get_screenshot to check visual correctness. Look closely for cropped/clipped text (line heights cutting off content) and overlapping elements — these are common and easy to miss. 3. Identify the discrepancy — is it structural (wrong hierarchy, missing nodes) or visual (wrong colors, broken layout, clipped content)? 4. Write a targeted fix script that modifies only the broken parts — don't recreate everything.
For the full validation workflow, see Validation & Error Recovery.
7. Pre-Flight Checklist
Before submitting ANY use_figma call, verify:
- [ ] Code uses
returnto send data back (NOTfigma.closePlugin()) - [ ] Code is NOT wrapped in an async IIFE (auto-wrapped for you)
- [ ]
returnvalue includes structured data with actionable info (IDs, counts) - [ ] NO usage of
figma.notify()anywhere - [ ] NO usage of
console.log()as output (usereturninstead) - [ ] All colors use 0–1 range (not 0–255)
- [ ] Fills/strokes are reassigned as new arrays (not mutated in place)
- [ ] Page switches use
await figma.setCurrentPageAsync(page)(sync setter throws) - [ ]
layoutSizingVertical/Horizontal = 'FILL'is set AFTERparent.appendChild(child) - [ ]
loadFontAsync()called BEFORE any text property changes - [ ]
lineHeight/letterSpacinguse{unit, value}format (not bare numbers) - [ ]
resize()is called BEFORE setting sizing modes (resize resets them to FIXED) - [ ] For multi-step workflows: IDs from previous calls are passed as string literals (not variables)
- [ ] New top-level nodes are positioned away from (0,0) to avoid overlapping existing content
- [ ] ALL created/mutated node IDs are collected and included in the
returnvalue - [ ] Every async call (
loadFontAsync,setCurrentPageAsync,importComponentByKeyAsync, etc.) isawaited — no fire-and-forget Promises
8. Discover Conventions Before Creating
Always inspect the Figma file before creating anything. Different files use different naming conventions, variable structures, and component patterns. Your code should match what's already there, not impose new conventions.
When in doubt about any convention (naming, scoping, structure), check the Figma file first, then the user's codebase. Only fall back to common patterns when neither exists.
Quick inspection scripts
List all pages and top-level nodes:
const pages = figma.root.children.map(p => `${p.name} id=${p.id} children=${p.children.length}`);
return pages.join('\n');List existing components across all pages:
const results = [];
for (const page of figma.root.children) {
await figma.setCurrentPageAsync(page);
page.findAll(n => {
if (n.type === 'COMPONENT' || n.type === 'COMPONENT_SET')
results.push(`[${page.name}] ${n.name} (${n.type}) id=${n.id}`);
return false;
});
}
return results.join('\n');List existing variable collections and their conventions:
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const results = collections.map(c => ({
name: c.name, id: c.id,
varCount: c.variableIds.length,
modes: c.modes.map(m => m.name)
}));
return results;9. Reference Docs
Load these as needed based on what your task involves:
| Doc | When to load | What it covers |
|---|---|---|
| gotchas.md | Before any use_figma | Every known pitfall with WRONG/CORRECT code examples |
| common-patterns.md | Need working code examples | Script scaffolds: shapes, text, auto-layout, variables, components, multi-step workflows |
| plugin-api-patterns.md | Creating/editing nodes | Fills, strokes, Auto Layout, effects, grouping, cloning, styles |
| api-reference.md | Need exact API surface | Node creation, variables API, core properties, what works and what doesn't |
| validation-and-recovery.md | Multi-step writes or error recovery | get_metadata vs get_screenshot workflow, mandatory error recovery steps |
| component-patterns.md | Creating components/variants | combineAsVariants, component properties, INSTANCE_SWAP, variant layout, discovering existing components, metadata traversal |
| variable-patterns.md | Creating/binding variables | Collections, modes, scopes, aliasing, binding patterns, discovering existing variables |
| text-style-patterns.md | Creating/applying text styles | Type ramps, font probing, listing styles, applying styles to nodes |
| effect-style-patterns.md | Creating/applying effect styles | Drop shadows, listing styles, applying styles to nodes |
| plugin-api-standalone.index.md | Need to understand the full API surface | Index of all types, methods, and properties in the Plugin API |
| plugin-api-standalone.d.ts | Need exact type signatures | Full typings file — grep for specific symbols, don't load all at once |
10. Snippet examples
You will see snippets throughout documentation here. These snippets contain useful plugin API code that can be repurposed. Use them as is, or as starter code as you go. If there are key concepts that are best documented as generic snippets, call them out and write to disk so you can reuse in the future.
interface:
display_name: "use_figma"
short_description: "Load the required rules before calling use_figma"
icon_small: "./assets/figma-small.svg"
icon_large: "./assets/figma.png"
default_prompt: "Use $figma-use and follow its rules before making any use_figma call."
dependencies:
tools:
- type: "mcp"
value: "figma"
description: "Figma MCP server"
transport: "streamable_http"
url: "https://mcp.figma.com/mcp"
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill="currentColor" fill-rule="evenodd" d="M4.994 5.986a2.014 2.014 0 1 0 0 4.028h2.069V5.986H4.994Zm5.063-.98h.055a2.014 2.014 0 1 0 0-4.026h-2.07v4.027h2.015Zm1.697.49A2.994 2.994 0 0 0 10.112 0H4.994a2.994 2.994 0 0 0-1.642 5.498A2.99 2.99 0 0 0 2 8a2.99 2.99 0 0 0 1.352 2.503A2.99 2.99 0 0 0 2 13.007C2 14.663 3.358 16 5.008 16c1.665 0 3.035-1.349 3.035-3.02v-2.765a2.984 2.984 0 0 0 2.014.778h.055a2.994 2.994 0 0 0 1.642-5.496Zm-1.642.49h-.055a2.014 2.014 0 1 0 0 4.028h.055a2.014 2.014 0 1 0 0-4.028Zm-7.132 7.02c0-1.111.902-2.013 2.014-2.013h2.069v1.987c0 1.123-.924 2.04-2.055 2.04a2.026 2.026 0 0 1-2.028-2.013Zm4.083-8H4.994a2.014 2.014 0 1 1 0-4.026h2.069v4.027Z" clip-rule="evenodd"/>
</svg>
<svg
width="400"
height="400"
viewBox="0 0 400 400"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M97.5 302.5C97.5 274.195 120.445 251.25 148.75 251.25H200V302.5C200 330.805 177.055 353.75 148.75 353.75C120.445 353.75 97.5 330.805 97.5 302.5Z"
fill="#0ACF83"
/>
<path
d="M200 200C200 171.696 222.945 148.75 251.25 148.75C279.554 148.75 302.5 171.695 302.5 200C302.5 228.305 279.554 251.25 251.25 251.25C222.945 251.25 200 228.304 200 200Z"
fill="#1ABCFE"
/>
<path
d="M97.5 200C97.5 228.305 120.445 251.25 148.75 251.25H200V148.75H148.75C120.445 148.75 97.5 171.695 97.5 200Z"
fill="#A259FF"
/>
<path
d="M200 46.25V148.75H251.25C279.555 148.75 302.5 125.805 302.5 97.5C302.5 69.1954 279.555 46.25 251.25 46.25H200Z"
fill="#FF7262"
/>
<path
d="M97.5 97.5C97.5 125.805 120.445 148.75 148.75 148.75H200V46.25L148.75 46.25C120.445 46.25 97.5 69.1954 97.5 97.5Z"
fill="#F24E1E"
/>
</svg>
Use of these Figma skills and related files ("Materials") is governed by the Figma Developer Terms (available at https://www.figma.com/legal/developer-terms/). By accessing, downloading, or using these Materials — including through automated systems or AI agents — you agree to the Figma Developer Terms.
These Materials are currently offered as a Beta feature. Figma may modify, suspend, or discontinue them at any time without notice.SKILL.md: mcp_server
Figma Plugin API Reference
Part of the use_figma skill. What works and what doesn't in the use_figma environment.Contents
- Node Creation
- Grouping and Boolean Operations
- Library Imports
- Variables API
- Core Properties
- Node Manipulation
- Descriptions and Documentation Links
- SVG and Images
- Utilities and Plugin Lifecycle
- Node Traversal
- Unsupported APIs
Node Creation (Design Mode)
figma.createRectangle()
figma.createFrame()
figma.createComponent() // Creates a ComponentNode
figma.createText()
figma.createEllipse()
figma.createStar()
figma.createLine()
figma.createVector()
figma.createPolygon()
figma.createBooleanOperation()
figma.createSlice()
figma.createPage() // Page node can be created, but child persistence is limited in headless mode
figma.createSection()
figma.createTextPath()Grouping & Boolean Operations
figma.group(nodes, parent, index?) // Group nodes
figma.flatten(nodes, parent?, index?) // Flatten to vector
figma.union(nodes, parent?, index?) // Boolean union
figma.subtract(nodes, parent?, index?) // Boolean subtract
figma.intersect(nodes, parent?, index?) // Boolean intersect
figma.exclude(nodes, parent?, index?) // Boolean exclude
figma.combineAsVariants(components, parent?) // Combine ComponentNodes into ComponentSet (Design/Sites only)Library Component Import
These methods import components from team libraries (not the same file you're working in). For components in the current file, use use_figma with figma.getNodeByIdAsync() or findOne()/findAll() to locate them directly.
// Import a published component from a team library by key
const comp = await figma.importComponentByKeyAsync("COMPONENT_KEY")
const instance = comp.createInstance()
// Import a published component set from a team library by key
const compSet = await figma.importComponentSetByKeyAsync("COMPONENT_SET_KEY")
const variant =
compSet.children.find((c) => c.type === "COMPONENT" && c.name.includes("size=md")) ||
compSet.defaultVariant
const variantInstance = variant.createInstance()Library Style Import (Team Libraries)
These methods import styles from team libraries (not the same file). For styles in the current file, use figma.getLocalPaintStyles(), figma.getLocalTextStyles(), etc.
// Import a published style from a team library by key
const style = await figma.importStyleByKeyAsync("STYLE_KEY")
// Apply the imported style to a node
await node.setFillStyleIdAsync(style.id) // for PaintStyle as fill
await node.setStrokeStyleIdAsync(style.id) // for PaintStyle as stroke
await node.setTextStyleIdAsync(style.id) // for TextStyle
await node.setEffectStyleIdAsync(style.id) // for EffectStyle
await node.setGridStyleIdAsync(style.id) // for GridStyleLibrary Variable Import (Team Libraries)
This imports variables from team libraries (not the same file). For variables in the current file, use figma.variables.getLocalVariables() or figma.variables.getVariableById().
// Import a published variable from a team library by key
const variable = await figma.variables.importVariableByKeyAsync("VARIABLE_KEY")
// Bind the imported variable to node properties
node.setBoundVariable("width", variable) // FLOAT variable
// Bind to fills/strokes (COLOR variable) — returns a NEW paint, must capture it
const newPaint = figma.variables.setBoundVariableForPaint(paintCopy, "color", variable)
node.fills = [newPaint]Variables API
// Collections
const collection = figma.variables.createVariableCollection("Name")
collection.name // Get/set name
collection.modes // Array of {modeId, name} — starts with 1 mode
collection.addMode("Dark") // Returns new modeId string
collection.renameMode(modeId, "Light")
// Variables
const variable = figma.variables.createVariable("name", collection, "COLOR")
// ^ object or ID string
// resolvedType: "COLOR" | "FLOAT" | "STRING" | "BOOLEAN"
variable.setValueForMode(modeId, value)
// Scopes — controls where variable appears in property pickers
variable.scopes = ["FRAME_FILL", "SHAPE_FILL"] // only fill pickers
variable.scopes = ["TEXT_FILL"] // only text color picker
variable.scopes = ["STROKE_COLOR"] // only stroke picker
variable.scopes = [] // hidden from all pickers (use for primitives)
// All valid scope values:
// ALL_SCOPES, TEXT_CONTENT, CORNER_RADIUS, WIDTH_HEIGHT, GAP,
// ALL_FILLS, FRAME_FILL, SHAPE_FILL, TEXT_FILL,
// STROKE_COLOR, STROKE_FLOAT, EFFECT_FLOAT, EFFECT_COLOR,
// OPACITY, FONT_FAMILY, FONT_STYLE, FONT_WEIGHT, FONT_SIZE,
// LINE_HEIGHT, LETTER_SPACING, PARAGRAPH_SPACING, PARAGRAPH_INDENT
// Querying
figma.variables.getVariableById(id)
figma.variables.getLocalVariables(resolvedType?)
figma.variables.getVariableCollectionById(id)
figma.variables.getLocalVariableCollections()
// Binding variables to paints (COLOR variables)
const newPaint = figma.variables.setBoundVariableForPaint(paintCopy, "color", variable)
// ⚠️ Returns a NEW paint — must capture return value!
node.fills = [newPaint]
// Binding variables to effects (COLOR/FLOAT variables)
const newEffect = figma.variables.setBoundVariableForEffect(effectCopy, field, variable)
// field for shadows: "color" (COLOR), "radius" | "spread" | "offsetX" | "offsetY" (FLOAT)
// field for blurs: "radius" (FLOAT)
// ⚠️ Returns a NEW effect — must capture return value!
node.effects = [newEffect]
// Binding variables to layout grids (FLOAT variables)
const newGrid = figma.variables.setBoundVariableForLayoutGrid(gridCopy, field, variable)
// field: "sectionSize" | "offset" | "count" | "gutterSize"
// ⚠️ Returns a NEW layout grid — must capture return value!
node.layoutGrids = [newGrid]
// Binding variables to node properties (FLOAT/STRING/BOOLEAN)
// Layout & sizing (FLOAT):
node.setBoundVariable("width", variable)
node.setBoundVariable("height", variable)
node.setBoundVariable("minWidth", variable)
node.setBoundVariable("maxWidth", variable)
node.setBoundVariable("minHeight", variable)
node.setBoundVariable("maxHeight", variable)
node.setBoundVariable("paddingLeft", variable)
node.setBoundVariable("paddingRight", variable)
node.setBoundVariable("paddingTop", variable)
node.setBoundVariable("paddingBottom", variable)
node.setBoundVariable("itemSpacing", variable)
node.setBoundVariable("counterAxisSpacing", variable)
// Corner radii (FLOAT) — use individual corners, NOT cornerRadius:
node.setBoundVariable("topLeftRadius", variable)
node.setBoundVariable("topRightRadius", variable)
node.setBoundVariable("bottomLeftRadius", variable)
node.setBoundVariable("bottomRightRadius", variable)
// Other (FLOAT):
node.setBoundVariable("opacity", variable)
node.setBoundVariable("strokeWeight", variable)
// ⚠️ fontSize, fontWeight, lineHeight are NOT bindable via setBoundVariable
// — set these directly as values on text nodes
// Aliases
figma.variables.createVariableAlias(variable)
// Explicit modes — CRITICAL for variant components
node.setExplicitVariableModeForCollection(collectionId, modeId)
// Without this, all nodes use the default (first) mode of the collectionCore Properties
figma.root // DocumentNode
figma.currentPage // Current page (read-only in use_figma; sync setter throws)
figma.setCurrentPageAsync(page) // Switch page and load its content (MUST await)
figma.fileKey // File key string
figma.mixed // Mixed sentinel valueNode Manipulation
// Fills & Strokes (read-only arrays — must clone)
node.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]
node.strokes = [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }]
node.strokeWeight = 1
node.strokeAlign = 'INSIDE' // 'INSIDE' | 'CENTER' | 'OUTSIDE'
// Effects
node.effects = [{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.25}, offset:{x:0,y:4}, radius:4, visible:true }]
// Layout
node.layoutMode = 'HORIZONTAL' // 'NONE' | 'HORIZONTAL' | 'VERTICAL'
node.primaryAxisAlignItems = 'CENTER' // 'MIN' | 'CENTER' | 'MAX' | 'SPACE_BETWEEN'
node.counterAxisAlignItems = 'CENTER' // 'MIN' | 'CENTER' | 'MAX' | 'BASELINE'
node.paddingLeft = 8
node.paddingRight = 8
node.paddingTop = 4
node.paddingBottom = 4
node.itemSpacing = 4
node.layoutSizingHorizontal = 'HUG' // 'FIXED' | 'HUG' | 'FILL'
node.layoutSizingVertical = 'HUG' // 'FIXED' | 'HUG' | 'FILL'
// Sizing
node.resize(width, height) // ⚠️ Resets sizing modes to FIXED
node.resizeWithoutConstraints(width, height) // Doesn't affect constraints
// Corner radius
node.cornerRadius = 8
// Visibility & Opacity
node.visible = true
node.opacity = 0.5
// Naming & Hierarchy
node.name = "My Node"
parent.appendChild(child)
parent.insertChild(index, child)
node.remove()Descriptions & Documentation Links
// Description — plain text, shown in Figma's component panel
node.description = "A short summary of this component's purpose and usage."
// Documentation links — array of {uri, label} shown as clickable links
componentSet.documentationLinks = [
{ uri: "https://example.com/docs", label: "Component Docs" }
]
// ⚠️ uri MUST be a valid URL (https://...) — relative paths will throwSVG Import
const svgNode = figma.createNodeFromSvg('<svg>...</svg>')Images
const image = figma.createImage(uint8Array)
node.fills = [{ type: 'IMAGE', scaleMode: 'FILL', imageHash: image.hash }]Utilities
figma.base64Encode(uint8Array) // Uint8Array → base64 string
figma.base64Decode(base64String) // base64 string → Uint8Array
figma.createComponentFromNode(node) // Convert existing node to component (Design/Sites only)Plugin Lifecycle
figma.closePlugin("message") // Close and return a message to the agent (success)
figma.closePluginWithFailure("error msg") // Close with error — ALWAYS use in catch blocksNode Traversal
node.findAll(pred?) // Find all descendants matching predicate
node.findOne(pred?) // Find first descendant matching predicate
node.findChildren(pred?) // Find direct children matching predicate
node.findChild(pred?) // Find first direct child matching predicate
node.children // Direct children array
node.parent // Parent node---
What Does NOT Work
| API | Status |
|---|---|
figma.notify() | Throws "not implemented" — most common mistake |
figma.showUI() | No-op (silently ignored) |
figma.openExternal() | No-op (silently ignored) |
figma.listAvailableFontsAsync() | Not implemented |
figma.loadAllPagesAsync() | Not implemented |
figma.variables.extendLibraryCollectionByKeyAsync() | Not implemented |
figma.teamLibrary.* | Not implemented (requires LiveGraph) |
Common Patterns
Part of the use_figma skill. Working code examples for frequently used operations.
Contents
- Basic Script Structure
- Create a Styled Shape
- Create a Text Node
- Create Frame with Auto-Layout
- Create Variable Collections and Bindings
- Create Components and Import by Key
- Component Sets with Variable Modes
- Multi-Step Large ComponentSet Pattern
- Read Existing Nodes and Return Data
Basic Script Structure
(async () => {
try {
const createdNodeIds = []
const mutatedNodeIds = []
// Your code here — track every node you create or mutate
// createdNodeIds.push(newNode.id)
// mutatedNodeIds.push(existingNode.id)
figma.closePlugin(JSON.stringify({
success: true,
createdNodeIds,
mutatedNodeIds,
// Plus any other useful data for subsequent calls
count: createdNodeIds.length
}))
} catch (e) {
figma.closePluginWithFailure(e.toString())
}
})()Create a Styled Shape
(async () => {
try {
// Find clear space to the right of existing content
const page = figma.currentPage
let maxX = 0
for (const child of page.children) {
maxX = Math.max(maxX, child.x + child.width)
}
const rect = figma.createRectangle()
rect.name = "Blue Box"
rect.resize(200, 100)
rect.fills = [{ type: 'SOLID', color: { r: 0.047, g: 0.549, b: 0.914 } }]
rect.cornerRadius = 8
rect.x = maxX + 100 // offset from existing content
rect.y = 0
figma.currentPage.appendChild(rect)
figma.closePlugin(JSON.stringify({ nodeId: rect.id }))
} catch (e) {
figma.closePluginWithFailure(e.toString())
}
})()Create a Text Node
(async () => {
try {
// Find clear space to the right of existing content
const page = figma.currentPage
let maxX = 0
for (const child of page.children) {
maxX = Math.max(maxX, child.x + child.width)
}
await figma.loadFontAsync({ family: "Inter", style: "Regular" })
const text = figma.createText()
text.characters = "Hello World"
text.fontSize = 16
text.fills = [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }]
text.textAutoResize = 'WIDTH_AND_HEIGHT'
text.x = maxX + 100
text.y = 0
figma.currentPage.appendChild(text)
figma.closePlugin(JSON.stringify({ nodeId: text.id }))
} catch (e) {
figma.closePluginWithFailure(e.toString())
}
})()Create Frame with Auto-Layout
(async () => {
try {
// Find clear space to the right of existing content
const page = figma.currentPage
let maxX = 0
for (const child of page.children) {
maxX = Math.max(maxX, child.x + child.width)
}
const frame = figma.createFrame()
frame.name = "Card"
frame.layoutMode = 'VERTICAL'
frame.primaryAxisAlignItems = 'MIN'
frame.counterAxisAlignItems = 'MIN'
frame.paddingLeft = 16
frame.paddingRight = 16
frame.paddingTop = 12
frame.paddingBottom = 12
frame.itemSpacing = 8
frame.layoutSizingHorizontal = 'HUG'
frame.layoutSizingVertical = 'HUG'
frame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]
frame.cornerRadius = 8
frame.x = maxX + 100
frame.y = 0
figma.currentPage.appendChild(frame)
figma.closePlugin(JSON.stringify({ nodeId: frame.id }))
} catch (e) {
figma.closePluginWithFailure(e.toString())
}
})()Create Variable Collection with Multiple Modes
(async () => {
try {
const collection = figma.variables.createVariableCollection("Theme/Colors")
// Rename the default mode
collection.renameMode(collection.modes[0].modeId, "Light")
const darkModeId = collection.addMode("Dark")
const lightModeId = collection.modes[0].modeId
const bgVar = figma.variables.createVariable("bg", collection, "COLOR")
bgVar.setValueForMode(lightModeId, { r: 1, g: 1, b: 1, a: 1 })
bgVar.setValueForMode(darkModeId, { r: 0.1, g: 0.1, b: 0.1, a: 1 })
const textVar = figma.variables.createVariable("text", collection, "COLOR")
textVar.setValueForMode(lightModeId, { r: 0, g: 0, b: 0, a: 1 })
textVar.setValueForMode(darkModeId, { r: 1, g: 1, b: 1, a: 1 })
figma.closePlugin(JSON.stringify({
collectionId: collection.id,
lightModeId,
darkModeId,
bgVarId: bgVar.id,
textVarId: textVar.id
}))
} catch (e) {
figma.closePluginWithFailure(e.toString())
}
})()Bind Color Variable to a Fill
(async () => {
try {
const variable = figma.variables.getVariableById("VariableID:1:2")
const rect = figma.createRectangle()
const basePaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }
// setBoundVariableForPaint returns a NEW paint — capture it!
const boundPaint = figma.variables.setBoundVariableForPaint(basePaint, "color", variable)
rect.fills = [boundPaint]
figma.closePlugin(JSON.stringify({ nodeId: rect.id }))
} catch (e) {
figma.closePluginWithFailure(e.toString())
}
})()Create Component Variants with Component Properties
Component properties (TEXT, BOOLEAN, INSTANCE_SWAP) MUST be added inside the per-variant loop, BEFORE combineAsVariants. The component set inherits them from its children.
(async () => {
try {
await figma.loadFontAsync({ family: "Inter", style: "Regular" })
// Assume defaultIconComp is an existing icon component (discovered earlier)
const defaultIconComp = figma.getNodeById('ICON_COMPONENT_ID')
const components = []
const variants = ["primary", "secondary"]
for (const variant of variants) {
const comp = figma.createComponent()
comp.name = `variant=${variant}`
comp.layoutMode = 'HORIZONTAL'
comp.primaryAxisAlignItems = 'CENTER'
comp.counterAxisAlignItems = 'CENTER'
comp.paddingLeft = 12
comp.paddingRight = 12
comp.paddingTop = 8
comp.paddingBottom = 8
comp.layoutSizingHorizontal = 'HUG'
comp.layoutSizingVertical = 'HUG'
comp.cornerRadius = 6
comp.itemSpacing = 8
// TEXT property — label
const labelKey = comp.addComponentProperty('Label', 'TEXT', 'Button')
const label = figma.createText()
label.characters = "Button"
label.fontSize = 14
comp.appendChild(label)
label.componentPropertyReferences = { characters: labelKey }
// BOOLEAN + INSTANCE_SWAP — icon slot
const showIconKey = comp.addComponentProperty('Show Icon', 'BOOLEAN', false)
const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', defaultIconComp.id)
const iconInstance = defaultIconComp.createInstance()
comp.insertChild(0, iconInstance) // icon before label
iconInstance.componentPropertyReferences = {
visible: showIconKey,
mainComponent: iconSlotKey
}
components.push(comp)
}
const componentSet = figma.combineAsVariants(components, figma.currentPage)
componentSet.name = "Button"
// Layout variants in a row after combining (they stack at 0,0 by default)
const colW = 140
componentSet.children.forEach((child, i) => {
child.x = i * colW
child.y = 0
})
// Resize from actual child bounds — formula-based sizing is error-prone
let maxX = 0, maxY = 0
for (const c of componentSet.children) {
maxX = Math.max(maxX, c.x + c.width)
maxY = Math.max(maxY, c.y + c.height)
}
componentSet.resizeWithoutConstraints(maxX + 40, maxY + 40)
figma.closePlugin(JSON.stringify({
componentSetId: componentSet.id,
componentIds: components.map(c => c.id)
}))
} catch (e) {
figma.closePluginWithFailure(e.toString())
}
})()Import a Component by Key (Team Libraries)
importComponentByKeyAsync and importComponentSetByKeyAsync import components from team libraries (not the same file you're working in). For components in the current file, use figma.getNodeByIdAsync() or findOne()/findAll() to locate them directly.
(async () => {
try {
// Import a single published component by key
const comp = await figma.importComponentByKeyAsync("COMPONENT_KEY")
const instance = comp.createInstance()
instance.x = 40
instance.y = 40
figma.currentPage.appendChild(instance)
// Import a published component set by key and select a variant
const compSet = await figma.importComponentSetByKeyAsync("COMPONENT_SET_KEY")
const variant =
compSet.children.find((c) =>
c.type === "COMPONENT" && c.name.includes("size=md")
) || compSet.defaultVariant
const variantInstance = variant.createInstance()
variantInstance.x = 240
variantInstance.y = 40
figma.currentPage.appendChild(variantInstance)
figma.closePlugin(JSON.stringify({
componentId: comp.id,
componentSetId: compSet.id,
placedInstanceIds: [instance.id, variantInstance.id]
}))
} catch (e) {
figma.closePluginWithFailure(e.toString())
}
})()Component Set with Variable Modes (Full Pattern)
(async () => {
try {
await figma.loadFontAsync({ family: "Inter", style: "Medium" })
// 1. Create color collection with modes per variant
const colors = figma.variables.createVariableCollection("Component/Colors")
colors.renameMode(colors.modes[0].modeId, "primary")
const primaryMode = colors.modes[0].modeId
const secondaryMode = colors.addMode("secondary")
const bgVar = figma.variables.createVariable("bg", colors, "COLOR")
bgVar.setValueForMode(primaryMode, { r: 0, g: 0.4, b: 0.9, a: 1 })
bgVar.setValueForMode(secondaryMode, { r: 0, g: 0, b: 0, a: 0 })
const textVar = figma.variables.createVariable("text-color", colors, "COLOR")
textVar.setValueForMode(primaryMode, { r: 1, g: 1, b: 1, a: 1 })
textVar.setValueForMode(secondaryMode, { r: 0.1, g: 0.1, b: 0.1, a: 1 })
// 2. Create components with variable bindings
const modeMap = { primary: primaryMode, secondary: secondaryMode }
const components = []
for (const [variantName, modeId] of Object.entries(modeMap)) {
const comp = figma.createComponent()
comp.name = "variant=" + variantName
comp.layoutMode = "HORIZONTAL"
comp.primaryAxisAlignItems = "CENTER"
comp.counterAxisAlignItems = "CENTER"
comp.paddingLeft = 12; comp.paddingRight = 12
comp.layoutSizingHorizontal = "HUG"
comp.layoutSizingVertical = "HUG"
comp.cornerRadius = 6
// Bind background fill to variable
const bgPaint = figma.variables.setBoundVariableForPaint(
{ type: "SOLID", color: { r: 0, g: 0, b: 0 } }, "color", bgVar
)
comp.fills = [bgPaint]
// Add text with bound color
const label = figma.createText()
label.fontName = { family: "Inter", style: "Medium" }
label.characters = "Button"
label.fontSize = 14
const textPaint = figma.variables.setBoundVariableForPaint(
{ type: "SOLID", color: { r: 0, g: 0, b: 0 } }, "color", textVar
)
label.fills = [textPaint]
comp.appendChild(label)
// 3. CRITICAL: Set explicit mode so this variant renders correctly
comp.setExplicitVariableModeForCollection(colors.id, modeId)
components.push(comp)
}
// 4. Combine into component set
const componentSet = figma.combineAsVariants(components, figma.currentPage)
componentSet.name = "Button"
figma.closePlugin(JSON.stringify({
componentSetId: componentSet.id,
colorCollectionId: colors.id
}))
} catch (e) {
figma.closePluginWithFailure(e.toString())
}
})()Large ComponentSet with Variable Modes (Multi-Step Pattern)
For component sets with many variants (50+), split into multiple use_figma calls:
Call 1: Create variable collections and return IDs
(async () => {
try {
// Hex-to-0-1 helper
const hex = (h) => {
if (!h) return { r: 0, g: 0, b: 0, a: 0 }; // transparent
return {
r: parseInt(h.slice(1,3), 16) / 255,
g: parseInt(h.slice(3,5), 16) / 255,
b: parseInt(h.slice(5,7), 16) / 255,
a: 1
};
};
const coll = figma.variables.createVariableCollection("MyComponent/Colors");
coll.renameMode(coll.modes[0].modeId, "mode1");
const mode2Id = coll.addMode("mode2");
// Create variables from data map
const colorData = { "bg/default": ["#0B6BCB", "#636B74"], /* ... */ };
const modeOrder = ["mode1", "mode2"];
const modeIds = { mode1: coll.modes[0].modeId, mode2: mode2Id };
const varIds = {};
for (const [name, values] of Object.entries(colorData)) {
const v = figma.variables.createVariable(name, coll, "COLOR");
values.forEach((hex_val, i) => {
v.setValueForMode(modeIds[modeOrder[i]], hex_val ? hex(hex_val) : { r:0, g:0, b:0, a:0 });
});
varIds[name] = v.id;
}
// Return ALL IDs — needed by subsequent calls
figma.closePlugin(JSON.stringify({ collId: coll.id, modeIds, varIds }));
} catch (e) {
figma.closePluginWithFailure(e.toString());
}
})()Call 2: Create components using stored IDs, combine and layout
(async () => {
try {
await figma.loadFontAsync({ family: "Inter", style: "Semi Bold" });
// Paste IDs from Call 1 as literals
const collId = "VariableCollectionId:X:Y";
const modeIds = { mode1: "X:0", mode2: "X:1" };
const varIds = { /* ... from Call 1 ... */ };
const getVar = (id) => figma.variables.getVariableById(id);
const bindColor = (varId) => figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', getVar(varId)
);
const components = [];
for (const mode of ["mode1", "mode2"]) {
for (const state of ["default", "hover"]) {
const comp = figma.createComponent();
comp.name = `mode=${mode}, state=${state}`;
comp.layoutMode = 'HORIZONTAL';
comp.primaryAxisAlignItems = 'CENTER';
comp.counterAxisAlignItems = 'CENTER';
comp.layoutSizingHorizontal = 'HUG';
comp.layoutSizingVertical = 'HUG';
comp.fills = [bindColor(varIds[`bg/${state}`])];
comp.setExplicitVariableModeForCollection(collId, modeIds[mode]);
// ... add text children ...
components.push(comp);
}
}
// Combine — all children stack at (0,0)!
const cs = figma.combineAsVariants(components, figma.currentPage);
cs.name = "MyComponent";
// CRITICAL: layout variants in a structured grid mapped to variant axes.
const stateOrder = ["default", "hover"];
const modeOrder2 = ["mode1", "mode2"];
const colW = 140, rowH = 56;
for (const child of cs.children) {
const props = Object.fromEntries(
child.name.split(', ').map(p => p.split('='))
);
const col = stateOrder.indexOf(props.state);
const row = modeOrder2.indexOf(props.mode);
child.x = col * colW;
child.y = row * rowH;
}
// Resize from actual child bounds
let maxX = 0, maxY = 0;
for (const child of cs.children) {
maxX = Math.max(maxX, child.x + child.width);
maxY = Math.max(maxY, child.y + child.height);
}
cs.resizeWithoutConstraints(maxX + 40, maxY + 40);
// Wrap in section
const section = figma.createSection();
section.name = "MyComponent Section";
section.appendChild(cs);
section.resizeWithoutConstraints(cs.width + 200, cs.height + 200);
figma.closePlugin(JSON.stringify({ csId: cs.id, count: components.length }));
} catch (e) {
figma.closePluginWithFailure(e.toString());
}
})()Read Existing Nodes and Return Data
(async () => {
try {
const page = figma.currentPage
const nodes = page.findAll(n => n.type === 'FRAME')
const data = nodes.map(n => ({
id: n.id,
name: n.name,
width: n.width,
height: n.height,
childCount: n.children?.length || 0
}))
figma.closePlugin(JSON.stringify({ frames: data }))
} catch (e) {
figma.closePluginWithFailure(e.toString())
}
})()Component & Variant API Patterns
Part of the use_figma skill. How to correctly use the Plugin API for components, variants, and component properties.
>
For design system context (when to use variants vs properties, code-to-Figma translation, property model), see wwds-components.
Contents
- Creating a Component
- Combining Components into a Component Set (Variants)
- Laying Out Variants After combineAsVariants (Required)
- Component Properties: addComponentProperty API
- Linking Properties to Child Nodes (Required)
- INSTANCE_SWAP: Avoiding Variant Explosion
- Discovering Existing Conventions in the File
- Importing Components by Key
- Working with Instances (finding variants, setProperties, text overrides, detachInstance)
Creating a Component
figma.createComponent() returns a ComponentNode, which behaves like a FrameNode but can be published, instanced, and combined into variant sets.
const comp = figma.createComponent();
comp.name = "MyComponent";
comp.layoutMode = "HORIZONTAL";
comp.primaryAxisAlignItems = "CENTER";
comp.counterAxisAlignItems = "CENTER";
comp.paddingLeft = 12;
comp.paddingRight = 12;
comp.layoutSizingHorizontal = "HUG";
comp.layoutSizingVertical = "HUG";
comp.fills = [{ type: "SOLID", color: { r: 0.2, g: 0.36, b: 0.96 } }];Combining Components into a Component Set (Variants)
figma.combineAsVariants(components, parent) takes an array of ComponentNodes (not frames — frames will throw) and groups them into a ComponentSetNode.
Variant names use a Property=Value format. Every unique combination must exist as a child component — missing ones show as blank gaps in the variant picker.
// Each component's name encodes its variant properties
const comp1 = figma.createComponent();
comp1.name = "size=md, style=primary";
const comp2 = figma.createComponent();
comp2.name = "size=md, style=secondary";
const componentSet = figma.combineAsVariants([comp1, comp2], figma.currentPage);
componentSet.name = "Button";Before creating variants, inspect the file for existing naming patterns. Different files use different conventions (State=Default vs state=default vs State/Default). Always match what's already there.
Laying Out Variants After combineAsVariants (Required)
After combineAsVariants, all children stack at (0, 0). You must position them or the component set will appear as a single collapsed element with all variants overlapping.
const cs = figma.combineAsVariants(components, figma.currentPage);
// Simple row layout
cs.children.forEach((child, i) => {
child.x = i * 150;
child.y = 0;
});
// CRITICAL: resize the component set from actual child bounds
let maxX = 0, maxY = 0;
for (const child of cs.children) {
maxX = Math.max(maxX, child.x + child.width);
maxY = Math.max(maxY, child.y + child.height);
}
cs.resizeWithoutConstraints(maxX + 40, maxY + 40);For multi-axis variants (e.g., size × style × state), parse the child's name to determine grid position:
for (const child of cs.children) {
const props = Object.fromEntries(
child.name.split(', ').map(p => p.split('='))
);
const col = stateValues.indexOf(props.state);
const row = styleValues.indexOf(props.style);
child.x = col * colWidth;
child.y = row * rowHeight;
}Component Properties: addComponentProperty API
addComponentProperty adds a TEXT, BOOLEAN, or INSTANCE_SWAP property to a component. It returns a string key (e.g., "label#4:0") — never hardcode or guess this key.
// Returns the key as a string — capture it!
const labelKey = comp.addComponentProperty('Label', 'TEXT', 'Default text');
const showIconKey = comp.addComponentProperty('Show Icon', 'BOOLEAN', true);
const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', iconComponentId);Timing: Add component properties to each variant component before calling combineAsVariants. After combining, the component set inherits all properties from its children. Do not add properties to the ComponentSetNode directly.
Linking Properties to Child Nodes (Required)
A property that is added but not linked to a child node does nothing. You must set componentPropertyReferences on the child:
// TEXT property → link to a text node's characters
const labelKey = comp.addComponentProperty('Label', 'TEXT', 'Button');
const textNode = figma.createText();
textNode.characters = "Button";
comp.appendChild(textNode);
textNode.componentPropertyReferences = { characters: labelKey };
// BOOLEAN + INSTANCE_SWAP → link to an instance node
const showIconKey = comp.addComponentProperty('Show Icon', 'BOOLEAN', true);
const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', iconComp.id);
const iconInstance = iconComp.createInstance();
comp.appendChild(iconInstance);
iconInstance.componentPropertyReferences = {
visible: showIconKey, // BOOLEAN controls show/hide
mainComponent: iconSlotKey // INSTANCE_SWAP controls which component
};Valid `componentPropertyReferences` keys:
characters— TEXT property on a TextNodevisible— BOOLEAN property (any node)mainComponent— INSTANCE_SWAP property on an InstanceNode
INSTANCE_SWAP: Avoiding Variant Explosion
When a component has many possible sub-elements (e.g., 30 different icons), never create a variant per sub-element. Use a single INSTANCE_SWAP property instead — the user picks from any compatible component at design time.
// Create icon as its own ComponentNode
const iconComp = figma.createComponent();
iconComp.name = "Icon/Search";
iconComp.resize(24, 24);
const svgNode = figma.createNodeFromSvg('<svg>...</svg>');
iconComp.appendChild(svgNode);
// Use it as the default for INSTANCE_SWAP
const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', iconComp.id);
const instance = iconComp.createInstance();
comp.appendChild(instance);
instance.componentPropertyReferences = { mainComponent: iconSlotKey };This works for icons, avatars, badges, or any swappable nested element.
Discovering Existing Conventions in the File
Always inspect the file before creating components. Different files have different naming styles, structures, and conventions. Your code should match what's already there.
List all existing components across all pages
(async () => {
try {
const results = [];
for (const page of figma.root.children) {
await figma.setCurrentPageAsync(page);
page.findAll(n => {
if (n.type === 'COMPONENT') results.push(`[${page.name}] ${n.name} (COMPONENT) id=${n.id}`);
if (n.type === 'COMPONENT_SET') results.push(`[${page.name}] ${n.name} (COMPONENT_SET) id=${n.id}`);
return false;
});
}
figma.closePlugin(results.join('\n'));
} catch(e) { figma.closePluginWithFailure(e.toString()); }
})()Inspect an existing component set's variant naming pattern
(async () => {
try {
const cs = await figma.getNodeByIdAsync('COMPONENT_SET_ID');
const variantNames = cs.children.map(c => c.name);
const propDefs = cs.componentPropertyDefinitions;
figma.closePlugin(JSON.stringify({ variantNames, propDefs }));
} catch(e) { figma.closePluginWithFailure(e.toString()); }
})()Find existing components in the file
(async () => {
try {
const components = [];
for (const page of figma.root.children) {
await figma.setCurrentPageAsync(page);
page.findAll(n => {
if (n.type === 'COMPONENT') {
components.push({ name: n.name, id: n.id, page: page.name, w: n.width, h: n.height });
}
return false;
});
}
figma.closePlugin(JSON.stringify(components));
} catch(e) { figma.closePluginWithFailure(e.toString()); }
})()Importing Components by Key (Team Libraries)
importComponentByKeyAsync and importComponentSetByKeyAsync import components from team libraries (not the same file you're working in). For components in the current file, use figma.getNodeByIdAsync() or findOne()/findAll() to locate them directly.
// Import a component from a team library
const comp = await figma.importComponentByKeyAsync("COMPONENT_KEY");
const instance = comp.createInstance();
// Import a component set from a team library and pick a variant
const set = await figma.importComponentSetByKeyAsync("COMPONENT_SET_KEY");
const variant = set.children.find(c =>
c.type === "COMPONENT" && c.name.includes("size=md")
) || set.defaultVariant;
const variantInstance = variant.createInstance();Working with Instances
Finding the right variant in a component set
Parse variant names to match on multiple properties simultaneously:
const compSet = await figma.importComponentSetByKeyAsync("KEY");
const variant = compSet.children.find(c => {
const props = Object.fromEntries(
c.name.split(', ').map(p => p.split('='))
);
return props.variant === "primary" && props.size === "md";
}) || compSet.defaultVariant;
const instance = variant.createInstance();Setting variant properties on an instance
After creating an instance from a component set, you can set variant properties via setProperties:
const instance = defaultVariant.createInstance();
instance.setProperties({
"variant": "primary",
"size": "medium"
});Overriding text in a component instance
Always discover component properties BEFORE writing text overrides. Components expose text as TEXT-type component properties, and setProperties() is the correct way to override them. Direct node.characters changes on property-managed text may be overridden by the component property system on render.
Step 1: Inspect componentProperties on a sample instance:
const instance = comp.createInstance();
const propDefs = instance.componentProperties;
// Returns e.g.: { "Label#2:0": { type: "TEXT", value: "Button" }, "Has Icon#4:64": { type: "BOOLEAN", value: true } }
figma.closePlugin(JSON.stringify(propDefs));Also check nested instances — a parent component may not expose text properties directly, but its nested child instances might:
const nestedInstances = instance.findAll(n => n.type === "INSTANCE");
const nestedProps = nestedInstances.map(ni => ({
name: ni.name,
id: ni.id,
properties: ni.componentProperties
}));Step 2: Use setProperties() for TEXT-type properties:
const instance = comp.createInstance();
const propDefs = instance.componentProperties;
for (const [key, def] of Object.entries(propDefs)) {
if (def.type === "TEXT") {
instance.setProperties({ [key]: "New text value" });
}
}For nested instances that expose their own TEXT properties, call setProperties() on the nested instance:
const nestedHeading = instance.findOne(n => n.type === "INSTANCE" && n.name === "Text Heading");
if (nestedHeading) {
nestedHeading.setProperties({ "Text#2104:5": "Actual heading text" });
}Step 3: Only fall back to direct node.characters for unmanaged text. If text is NOT controlled by any component property, find text nodes directly. Always load the node's actual font first — instance text nodes inherit fonts from the source component, so don't assume Inter Regular:
const textNodes = instance.findAll(n => n.type === "TEXT");
for (const t of textNodes) {
await figma.loadFontAsync(t.fontName);
t.characters = "Updated text";
}detachInstance() invalidates ancestor node IDs
Warning: When detachInstance() is called on a nested instance inside a library component instance, the parent instance may also get implicitly detached (converted from INSTANCE to FRAME with a new ID). Subsequent getNodeByIdAsync(oldParentId) returns null.
// WRONG — cached parent ID becomes invalid after child detach
const parentId = parentInstance.id;
nestedChild.detachInstance();
const parent = await figma.getNodeByIdAsync(parentId); // null!
// CORRECT — re-discover nodes by traversal from a stable (non-instance) parent
const stableFrame = await figma.getNodeByIdAsync(manualFrameId); // a frame YOU created
nestedChild.detachInstance();
// Re-find the parent by traversing from the stable frame
const parent = stableFrame.findOne(n => n.name === "ParentName");If you must detach multiple nested instances across sibling components, do it in a single use_figma call — discover all targets by traversal at the start before any detachment mutates the tree.
Inspecting Component Metadata (Deep Traversal)
These helpers extract the full property schema and descendant structure of a component. Useful for understanding complex components before creating instances or setting properties.
/**
* Imports a component or component set from a library by its published key.
* Tries COMPONENT first, then falls back to COMPONENT_SET.
*
* @param {string} componentKey - The published key of the component or component set.
* @returns {Promise<ComponentNode|ComponentSetNode>}
*/
async function importComponentByKey(componentKey) {
try {
return await figma.importComponentByKeyAsync(componentKey);
} catch {
try {
return await figma.importComponentSetByKeyAsync(componentKey);
} catch {
throw new Error(`No Component or Component Set available with key '${componentKey}'`);
}
}
}
/**
* Given a main component node, returns the component set parent if one exists,
* otherwise returns the component itself. Used to get the top-level node that
* holds `componentPropertyDefinitions`.
*
* @param {ComponentNode} mainComponent
* @returns {ComponentNode|ComponentSetNode}
*/
function getRelevantComponentNode(mainComponent) {
return mainComponent.parent.type === "COMPONENT_SET"
? mainComponent.parent
: mainComponent;
}
/**
* Extracts `componentPropertyDefinitions` from a component or component set node
* into a flat map keyed by property key.
*
* @param {ComponentNode|ComponentSetNode} node
* @returns {Record<string, {name: string, type: string, key: string, variantOptions?: string[]}>}
*/
function getComponentProps(node) {
const result = {};
for (let key in node.componentPropertyDefinitions) {
const prop = {
name: key.replace(/#[^#]+$/, ""),
type: node.componentPropertyDefinitions[key].type,
key: key
};
if (prop.type === "VARIANT") {
prop.variantOptions = node.componentPropertyDefinitions[key].variantOptions;
}
result[key] = prop;
}
return result;
}
/**
* Recursively walks a component tree and collects all INSTANCE and TEXT nodes
* into `result`, keyed by `TYPE[name]`. Handles variant namespacing and
* deduplicates nodes with identical names but differing property references.
*
* @param {SceneNode} node - The node to traverse.
* @param {string[]} namespace - Accumulated variant names for the current path.
* @param {Record<string, object>} result - Accumulator object populated in place.
*/
function collectDescendants(node, namespace, result) {
if (node.type === "INSTANCE" || node.type === "TEXT") {
const references = node.componentPropertyReferences || {};
if (!node.visible && !references.visible) return;
const object = { type: node.type, name: node.name, references };
let key = `${node.type}[${node.name}]`;
if (result[key] && JSON.stringify(references) !== JSON.stringify(result[key].references)) {
key += btoa(btoa(unescape(encodeURIComponent(JSON.stringify(references)))));
}
if (node.type === "INSTANCE") {
const mainComponent = getRelevantComponentNode(node.mainComponent);
object.properties = getComponentProps(mainComponent);
object.descendants = {};
object.mainComponentName = mainComponent.name;
collectDescendants(mainComponent, [], object.descendants);
}
const start = namespace.length ? { variants: [] } : {};
result[key] = Object.assign(object, result[key] || start);
if (namespace.length) result[key].variants.push(namespace[namespace.length - 1]);
} else if ("children" in node && node.visible) {
if (node.type === "COMPONENT" && node.parent.type === "COMPONENT_SET") namespace.push(node.name);
node.children.forEach(child => collectDescendants(child, namespace, result));
}
}
/**
* Returns structured metadata for a component or component set defined in the current file.
*
* @param {string} componentId - The node ID of a COMPONENT or COMPONENT_SET node.
* @returns {Promise<{name: string, nodeId: string, properties: object, descendants: object}|undefined>}
*/
async function getLocalComponentMetadata(componentId) {
const node = await figma.getNodeByIdAsync(componentId);
if (node.type === "COMPONENT_SET" || node.type === "COMPONENT") {
const result = {
name: node.name,
nodeId: node.id,
properties: {},
descendants: {}
};
result.properties = getComponentProps(node);
collectDescendants(node, [], result.descendants);
return result;
} else {
throw new Error("Node is not a Component or Component Set");
}
}
/**
* Returns structured metadata for a published component or component set loaded by its key.
*
* @param {string} componentKey - The published key of the component or component set.
* @returns {Promise<{name: string, nodeId: string, properties: object, descendants: object}>}
*/
async function getPublishedComponentMetadata(componentKey) {
const node = await importComponentByKey(componentKey);
const result = {
name: node.name,
nodeId: node.id,
properties: {},
descendants: {}
};
result.properties = getComponentProps(node);
collectDescendants(node, [], result.descendants);
return result;
}Full metadata extraction script
(async () => {
try {
// For local components, use getLocalComponentMetadata:
const result = await getLocalComponentMetadata('COMPONENT_OR_SET_ID');
figma.closePlugin(JSON.stringify(result));
// For published components, use getPublishedComponentMetadata:
// const result = await getPublishedComponentMetadata('COMPONENT_KEY');
// figma.closePlugin(JSON.stringify(result));
} catch(e) { figma.closePluginWithFailure(e.toString()); }
})()Effect Style API Patterns
Part of the use_figma skill. How to create, apply, and inspect effect styles using the Plugin API.
>
For design system context (effect types, variable bindings on effects, gotchas), see wwds-effect-styles.
Contents
- Listing Effect Styles
- Creating a Drop Shadow Style
- Applying Effect Styles to Nodes
Listing Effect Styles
/**
* Lists all local effect styles.
*
* @returns {Promise<Array<{id: string, name: string, key: string, effectCount: number}>>}
*/
async function listEffectStyles() {
const styles = await figma.getLocalEffectStylesAsync();
return styles.map(s => ({
id: s.id,
name: s.name,
key: s.key,
effectCount: s.effects.length
}));
}Full runnable script:
(async () => {
try {
const results = await listEffectStyles();
figma.closePlugin(JSON.stringify(results));
} catch(e) { figma.closePluginWithFailure(e.toString()); }
})()Creating a Drop Shadow Style
Colors are RGBA 0–1 range. effects is a read-only array — always reassign, never mutate in place.
/**
* Creates a drop shadow effect style.
*
* @param {string} name - e.g. "Elevation/200"
* @param {{ r: number, g: number, b: number, a: number }} color - RGBA, 0-1 range
* @param {{ x: number, y: number }} offset
* @param {number} radius - blur radius
* @param {number} [spread=0]
* @returns {EffectStyle}
*/
function createDropShadowStyle(name, color, offset, radius, spread) {
const style = figma.createEffectStyle();
style.name = name;
style.effects = [{
type: "DROP_SHADOW",
color,
offset,
radius,
spread: spread || 0,
visible: true,
blendMode: "NORMAL"
}];
return style;
}Full runnable script:
(async () => {
try {
const style = createDropShadowStyle(
"Elevation/200",
{ r: 0, g: 0, b: 0, a: 0.15 },
{ x: 0, y: 4 },
12,
0
);
figma.closePlugin(JSON.stringify({ id: style.id, name: style.name }));
} catch(e) { figma.closePluginWithFailure(e.toString()); }
})()Applying Effect Styles to Nodes
/**
* Applies an effect style to all nodes on the current page that match a given name pattern.
*
* @param {string} styleId - The ID of an EffectStyle.
* @param {string} nodeNamePattern - Substring match against node names.
* @returns {number} - Number of nodes the style was applied to.
*/
function applyEffectStyleToMatchingNodes(styleId, nodeNamePattern) {
const nodes = figma.currentPage.findAll(n => n.name.includes(nodeNamePattern));
let applied = 0;
for (const node of nodes) {
if ('effectStyleId' in node) {
node.effectStyleId = styleId;
applied++;
}
}
return applied;
}Full runnable script:
(async () => {
try {
const applied = applyEffectStyleToMatchingNodes('STYLE_ID', 'Card');
figma.closePlugin(JSON.stringify({ applied }));
} catch(e) { figma.closePluginWithFailure(e.toString()); }
})()Gotchas & Common Mistakes
Part of the use_figma skill. Every known pitfall with WRONG/CORRECT code examples.
Contents
- Component properties and variant creation pitfalls
- Paint, color, and variable binding pitfalls
- Page context and plugin lifecycle pitfalls
- Auto Layout and sizing order pitfalls (including HUG/FILL interactions)
- Variant layout and geometry pitfalls
- Variable scopes and mode pitfalls
- Node cleanup and empty-fill pitfalls
- detachInstance() and node ID invalidation
New nodes default to (0,0) and overlap existing content
Every figma.create*() call places the node at position (0,0). If you append multiple nodes directly to the page, they all stack on top of each other and on top of any existing content.
This only matters for nodes appended directly to the page (i.e., top-level nodes). Nodes appended as children of other frames, components, or auto-layout containers are positioned by their parent — don't scan for overlaps when nesting nodes.
// WRONG — top-level node lands at (0,0), overlapping existing page content
const frame = figma.createFrame()
frame.name = "My New Frame"
frame.resize(400, 300)
figma.currentPage.appendChild(frame)
// CORRECT — find existing content bounds and place the new top-level node to the right
const page = figma.currentPage
let maxX = 0
for (const child of page.children) {
const right = child.x + child.width
if (right > maxX) maxX = right
}
const frame = figma.createFrame()
frame.name = "My New Frame"
frame.resize(400, 300)
figma.currentPage.appendChild(frame)
frame.x = maxX + 100 // 100px gap from rightmost existing content
frame.y = 0
// NOT NEEDED — child nodes inside a parent don't need overlap scanning
const card = figma.createFrame()
card.layoutMode = 'VERTICAL'
const label = figma.createText()
card.appendChild(label) // positioned by auto-layout, no x/y neededaddComponentProperty returns a string key, not an object — never hardcode or guess it
Figma generates the property key dynamically (e.g. "label#4:0"). The suffix is unpredictable. Always capture and use the return value directly.
// WRONG — guessing / hardcoding the key
comp.addComponentProperty('label', 'TEXT', 'Button')
labelNode.componentPropertyReferences = { characters: 'label#0:1' } // Error: key not found
// WRONG — treating the return value as an object
const result = comp.addComponentProperty('Label', 'TEXT', 'Button')
const propKey = Object.keys(result)[0] // BUG: returns '0' (first char index of string!)
labelNode.componentPropertyReferences = { characters: propKey } // Error: property '0' not found
// CORRECT — the return value IS the key string, use it directly
const propKey = comp.addComponentProperty('Label', 'TEXT', 'Button')
// propKey === "label#4:0" (exact value varies; never assume it)
labelNode.componentPropertyReferences = { characters: propKey }The same applies to COMPONENT_SET nodes — addComponentProperty always returns the property key as a string.
MUST return ALL created/mutated node IDs
Every script that creates or mutates nodes on the canvas must track and return all affected node IDs in the figma.closePlugin() response. Without these IDs, subsequent calls cannot reference, validate, or clean up those nodes.
// WRONG — only returns the parent frame ID, loses track of children
const frame = figma.createFrame()
const rect = figma.createRectangle()
const text = figma.createText()
frame.appendChild(rect)
frame.appendChild(text)
figma.closePlugin(JSON.stringify({ nodeId: frame.id }))
// CORRECT — returns all created node IDs in a structured response
const frame = figma.createFrame()
const rect = figma.createRectangle()
const text = figma.createText()
frame.appendChild(rect)
frame.appendChild(text)
figma.closePlugin(JSON.stringify({
createdNodeIds: [frame.id, rect.id, text.id],
rootNodeId: frame.id
}))
// CORRECT — when mutating existing nodes, return those IDs too
const nodes = figma.currentPage.findAll(n => n.name === 'Card')
for (const n of nodes) {
n.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]
}
figma.closePlugin(JSON.stringify({
mutatedNodeIds: nodes.map(n => n.id),
count: nodes.length
}))Colors are 0–1 range
// WRONG — will throw validation error (ZeroToOne enforced)
node.fills = [{ type: 'SOLID', color: { r: 255, g: 0, b: 0 } }]
// CORRECT
node.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]Fills/strokes are immutable arrays
// WRONG — modifying in place does nothing
node.fills[0].color = { r: 1, g: 0, b: 0 }
// CORRECT — clone, modify, reassign
const fills = JSON.parse(JSON.stringify(node.fills))
fills[0].color = { r: 1, g: 0, b: 0 }
node.fills = fillssetBoundVariableForPaint returns a NEW paint
// WRONG — ignoring return value
figma.variables.setBoundVariableForPaint(paint, "color", colorVar)
node.fills = [paint] // paint is unchanged!
// CORRECT — capture the returned new paint
const boundPaint = figma.variables.setBoundVariableForPaint(paint, "color", colorVar)
node.fills = [boundPaint]Variable collection starts with 1 mode
// A new collection already has one mode — rename it, don't try to add first
const collection = figma.variables.createVariableCollection("Colors")
// collection.modes = [{ modeId: "...", name: "Mode 1" }]
collection.renameMode(collection.modes[0].modeId, "Light")
const darkModeId = collection.addMode("Dark")combineAsVariants requires ComponentNodes
// WRONG — passing frames
const f1 = figma.createFrame()
figma.combineAsVariants([f1], figma.currentPage) // Error!
// CORRECT — passing components
const c1 = figma.createComponent()
c1.name = "variant=primary, size=md"
const c2 = figma.createComponent()
c2.name = "variant=secondary, size=md"
figma.combineAsVariants([c1, c2], figma.currentPage)Page switching: sync setter throws
The sync setter figma.currentPage = page throws an error in use_figma runtimes (MCP, evals, assistant). Use await figma.setCurrentPageAsync(page) instead — it switches the page and loads its content.
// WRONG — throws "Setting figma.currentPage is not supported in this runtime"
figma.currentPage = targetPage
// CORRECT — async method switches and loads content
await figma.setCurrentPageAsync(targetPage)get_metadata only sees one page — use use_figma to discover all pages
A Figma file can have multiple pages (canvas nodes). get_metadata operates on a single node/page — it cannot scan the entire document. To discover all pages and their top-level contents, use use_figma:
// WRONG — calling get_metadata with the file root or expecting it to list all pages
// get_metadata only returns the subtree of the node you pass it
// CORRECT — use use_figma to list pages, then inspect each one
const pages = figma.root.children.map(p => `${p.name} id=${p.id} children=${p.children.length}`);
figma.closePlugin(pages.join('\n'));Icons, variables, and components may live on pages other than the first. Always enumerate all pages before concluding that the file has no existing assets.
Never use figma.notify()
// WRONG — throws "not implemented" error
figma.notify("Done!")
// CORRECT — use closePlugin for messaging
figma.closePlugin("Done!")Script must always terminate
// WRONG — no closePlugin call, script hangs
(async () => {
figma.createRectangle()
})()
// CORRECT — always close
(async () => {
try {
figma.createRectangle()
figma.closePlugin("created")
} catch(e) {
figma.closePluginWithFailure(e.toString())
}
})()setBoundVariable for paint fields only works on SOLID paints
// Only SOLID paint type supports color variable binding
// Gradient paints, image paints, etc. will throw
const solidPaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }
const bound = figma.variables.setBoundVariableForPaint(solidPaint, "color", colorVar)Explicit variable modes must be set per component
// WRONG — all variants render with the default (first) mode
const colorCollection = figma.variables.createVariableCollection("Colors")
// ... create variables and modes ...
// Components all show the first mode's values by default!
// CORRECT — set explicit mode on each component to get variant-specific values
component.setExplicitVariableModeForCollection(colorCollection.id, targetModeId)TextStyle.setBoundVariable is not available in headless use_figma
setBoundVariable exists on TextStyle in the typed API but is not available when running scripts through use_figma (MCP, headless assistant mode). Calling it will throw "not a function".
// WRONG — throws "not a function" in use_figma / headless
const ts = figma.createTextStyle()
ts.setBoundVariable("fontSize", fontSizeVar)
// CORRECT (headless) — set raw values; bind variables interactively in Figma later
const ts = figma.createTextStyle()
ts.fontSize = 24This only affects TextStyle. Variable binding on nodes (node.setBoundVariable(...)) and on paint objects (figma.variables.setBoundVariableForPaint(...)) still works in headless mode as expected.
If live variable binding on text styles is required, create the styles with raw values via use_figma, then bind variables interactively through the Figma Styles panel or a full interactive plugin.
lineHeight and letterSpacing must be objects, not bare numbers
// WRONG — throws or silently does nothing
style.lineHeight = 1.5
style.lineHeight = 24
style.letterSpacing = 0
// CORRECT
style.lineHeight = { unit: "AUTO" } // auto/intrinsic
style.lineHeight = { value: 24, unit: "PIXELS" } // fixed pixel height
style.lineHeight = { value: 150, unit: "PERCENT" } // percentage of font size
style.letterSpacing = { value: 0, unit: "PIXELS" } // no tracking
style.letterSpacing = { value: -0.5, unit: "PIXELS" } // tight
style.letterSpacing = { value: 5, unit: "PERCENT" } // percent-basedThis applies to both TextStyle and TextNode properties. The same rule applies inside use_figma, interactive plugins, and any other plugin API context.
Font style names are file-dependent — probe before assuming
Font style names vary per provider and per Figma file. "SemiBold" and "Semi Bold" are different strings. Loading a font with the wrong style string throws silently or errors — there is no canonical list.
// WRONG — guessing style names
await figma.loadFontAsync({ family: "Inter", style: "SemiBold" }) // may throw
// CORRECT — probe which style names are available
const candidates = ["SemiBold", "Semi Bold", "Semibold"]
for (const style of candidates) {
try {
await figma.loadFontAsync({ family: "Inter", style })
// capture the one that works
break
} catch (_) {}
}When building a type ramp script, always verify font styles against the target file before hardcoding them.
combineAsVariants does NOT auto-layout in headless mode
// WRONG — all variants stack at position (0, 0), resulting in a tiny ComponentSet
const components = [comp1, comp2, comp3]
const cs = figma.combineAsVariants(components, figma.currentPage)
// cs.width/height will be the size of a SINGLE variant!
// CORRECT — manually layout children in a grid after combining
const cs = figma.combineAsVariants(components, figma.currentPage)
const colWidth = 120
const rowHeight = 56
cs.children.forEach((child, i) => {
const col = i % numCols
const row = Math.floor(i / numCols)
child.x = col * colWidth
child.y = row * rowHeight
})
// CRITICAL: resize from actual child bounds, not formula — formula errors leave variants outside the boundary
let maxX = 0, maxY = 0
for (const child of cs.children) {
maxX = Math.max(maxX, child.x + child.width)
maxY = Math.max(maxY, child.y + child.height)
}
cs.resizeWithoutConstraints(maxX + 40, maxY + 40)COLOR variable values use {r, g, b, a} (with alpha)
// Paint colors use {r, g, b} (no alpha — opacity is a separate paint property)
node.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]
// But COLOR variable values use {r, g, b, a} — alpha maps to paint opacity
const colorVar = figma.variables.createVariable("bg", collection, "COLOR")
colorVar.setValueForMode(modeId, { r: 1, g: 0, b: 0, a: 1 }) // opaque red
colorVar.setValueForMode(modeId, { r: 0, g: 0, b: 0, a: 0 }) // fully transparent
// ⚠️ Don't confuse: {r, g, b} for paint colors vs {r, g, b, a} for variable valueslayoutSizingVertical/layoutSizingHorizontal = 'FILL' requires auto-layout parent FIRST
// WRONG — setting FILL before the node is a child of an auto-layout frame
const child = figma.createFrame()
child.layoutSizingVertical = 'FILL' // ERROR: "FILL can only be set on children of auto-layout frames"
parent.appendChild(child)
// CORRECT — append to auto-layout parent FIRST, then set FILL
const child = figma.createFrame()
parent.appendChild(child) // parent must have layoutMode set
child.layoutSizingVertical = 'FILL' // Works!HUG parents collapse FILL children
A HUG parent cannot give FILL children meaningful size. If children have layoutSizingHorizontal = "FILL" but the parent is "HUG", the children collapse to minimum size. The parent must be "FILL" or "FIXED" for FILL children to expand. This is a common cause of truncated text in select fields, inputs, and action rows.
// WRONG — parent hugs, so FILL children get zero extra space
const parent = figma.createFrame()
parent.layoutMode = 'HORIZONTAL'
parent.layoutSizingHorizontal = 'HUG'
const child = figma.createFrame()
parent.appendChild(child)
child.layoutSizingHorizontal = 'FILL' // collapses to min size!
// CORRECT — parent must be FIXED or FILL for FILL children to expand
const parent = figma.createFrame()
parent.layoutMode = 'HORIZONTAL'
parent.resize(400, 50)
parent.layoutSizingHorizontal = 'FIXED' // or 'FILL' if inside another auto-layout
const child = figma.createFrame()
parent.appendChild(child)
child.layoutSizingHorizontal = 'FILL' // expands to fill remaining 400pxlayoutGrow with a hugging parent causes content compression
// WRONG — layoutGrow on a child when parent has primaryAxisSizingMode='AUTO' (hug)
// causes the child to SHRINK below its natural size instead of expanding
const parent = figma.createComponent()
parent.layoutMode = 'VERTICAL'
parent.primaryAxisSizingMode = 'AUTO' // hug contents
const content = figma.createFrame()
content.layoutMode = 'VERTICAL'
content.primaryAxisSizingMode = 'AUTO'
parent.appendChild(content)
content.layoutGrow = 1 // BUG: content compresses, children hidden!
// CORRECT — only use layoutGrow when parent has FIXED sizing with extra space
content.layoutGrow = 0 // let content take its natural size
// OR: set parent to FIXED sizing first
parent.primaryAxisSizingMode = 'FIXED'
parent.resizeWithoutConstraints(300, 500)
content.layoutGrow = 1 // NOW it correctly fills remaining spaceresize() resets primaryAxisSizingMode and counterAxisSizingMode to FIXED
// WRONG — resize() after setting sizing mode overwrites it back to FIXED
const frame = figma.createComponent()
frame.layoutMode = 'VERTICAL'
frame.primaryAxisSizingMode = 'AUTO' // hug height
frame.counterAxisSizingMode = 'FIXED'
frame.resize(300, 10) // BUG: resets BOTH axes to 'FIXED'! Height stays at 10px forever.
// CORRECT — call resize() FIRST, then set sizing modes
const frame = figma.createComponent()
frame.layoutMode = 'VERTICAL'
frame.resize(300, 10) // set initial dimensions first
frame.counterAxisSizingMode = 'FIXED' // keep width fixed at 300
frame.primaryAxisSizingMode = 'AUTO' // NOW set height to hug — this sticks!
// Or use the modern shorthand (equivalent):
// frame.layoutSizingHorizontal = 'FIXED'
// frame.layoutSizingVertical = 'HUG'Node positions don't auto-reset after reparenting
// WRONG — assuming positions reset when moving a node into a new parent
const node = figma.createRectangle()
node.x = 500; node.y = 500;
figma.currentPage.appendChild(node)
section.appendChild(node) // node still at (500, 500) relative to section!
// CORRECT — explicitly set x/y after ANY reparenting operation
section.appendChild(node)
node.x = 80; node.y = 80; // reset to desired position within sectionGrid layout with mixed-width rows causes overlaps
// WRONG — using a single column offset for rows with different-width items
// e.g. vertical cards (320px) and horizontal cards (500px) in a 2-row grid
for (let i = 0; i < allCards.length; i++) {
allCards[i].x = (i % 4) * 370 // 370 works for 320px cards but NOT 500px cards!
}
// CORRECT — compute each row's spacing independently based on actual child widths
const gap = 50
let x = 0
for (const card of horizontalCards) {
card.x = x
x += card.width + gap // use actual width, not a fixed column size
}Sections don't auto-resize to fit content
// WRONG — section stays at default size, content overflows
const section = figma.createSection()
section.name = "My Section"
section.appendChild(someNode) // node may be outside section bounds
// CORRECT — explicitly resize after adding content
const section = figma.createSection()
section.name = "My Section"
section.appendChild(someNode)
section.resizeWithoutConstraints(
Math.max(someNode.width + 100, 800),
Math.max(someNode.height + 100, 600)
)counterAxisAlignItems does NOT support 'STRETCH'
// WRONG — 'STRETCH' is not a valid enum value
comp.counterAxisAlignItems = 'STRETCH'
// Error: Invalid enum value. Expected 'MIN' | 'MAX' | 'CENTER' | 'BASELINE', received 'STRETCH'
// CORRECT — use 'MIN' on the parent, then set children to FILL on the cross axis
comp.counterAxisAlignItems = 'MIN'
comp.appendChild(child)
// For vertical layout, stretch width:
child.layoutSizingHorizontal = 'FILL'
// For horizontal layout, stretch height:
child.layoutSizingVertical = 'FILL'Variable collection mode limits are plan-dependent
// Figma limits modes per collection based on the team/org plan:
// Free: 1 mode only (no addMode)
// Professional: up to 4 modes
// Organization/Enterprise: up to 40+ modes
//
// WRONG — creating 20 modes on a Professional plan will fail silently or throw
const coll = figma.variables.createVariableCollection("Variants")
for (let i = 0; i < 20; i++) coll.addMode("mode" + i) // May fail!
// CORRECT — if you need many modes, split across multiple collections
// E.g., instead of 1 collection with 20 modes (variant×color):
// Collection A: 4 modes (variant: plain/outlined/soft/solid)
// Collection B: 5 modes (color: neutral/primary/danger/success/warning)
// Then use setExplicitVariableModeForCollection for BOTH on each componentVariables default to ALL_SCOPES — always set scopes explicitly
// WRONG — variable appears in every property picker (fills, text, strokes, spacing, etc.)
const bgColor = figma.variables.createVariable("Background/Default", coll, "COLOR")
// bgColor.scopes defaults to ["ALL_SCOPES"] — pollutes all dropdowns
// CORRECT — restrict to relevant property pickers
const bgColor = figma.variables.createVariable("Background/Default", coll, "COLOR")
bgColor.scopes = ["FRAME_FILL", "SHAPE_FILL", "EFFECT_COLOR"] // fill pickers only
const textColor = figma.variables.createVariable("Text/Default", coll, "COLOR")
textColor.scopes = ["TEXT_FILL"] // text color picker only
const borderColor = figma.variables.createVariable("Border/Default", coll, "COLOR")
borderColor.scopes = ["STROKE_COLOR"] // stroke picker only
const spacing = figma.variables.createVariable("Space/400", coll, "FLOAT")
spacing.scopes = ["GAP"] // gap/spacing pickers only
// Hide primitives that are only referenced via aliases
const primitive = figma.variables.createVariable("Brand/500", coll, "COLOR")
primitive.scopes = [] // hidden from all pickersBinding fills on nodes with empty fills
// WRONG — binding to a node with no fills does nothing
const comp = figma.createComponent()
comp.fills = [] // transparent
// Can't bind a color variable to fills that don't exist
// CORRECT — add a placeholder SOLID fill, then bind the variable
const comp = figma.createComponent()
const basePaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }
const boundPaint = figma.variables.setBoundVariableForPaint(basePaint, "color", colorVar)
comp.fills = [boundPaint]
// The variable's resolved value (which may be transparent) will control the actual colorMode names must be descriptive — never leave 'Mode 1'
Every new VariableCollection starts with one mode named 'Mode 1'. Always rename it immediately. For single-mode collections use 'Default'; for multi-mode collections use names from the source (e.g. 'Light'/'Dark', 'Desktop'/'Tablet'/'Mobile').
// WRONG — generic names give no semantic meaning const coll = figma.variables.createVariableCollection('Colors') // coll.modes[0].name === 'Mode 1' — left as-is const darkId = coll.addMode('Mode 2')
// CORRECT — rename immediately to match the source const coll = figma.variables.createVariableCollection('Colors') coll.renameMode(coll.modes[0].modeId, 'Light') // was 'Mode 1' const darkId = coll.addMode('Dark')
// For single-mode collections (primitives, spacing, etc.) const spacing = figma.variables.createVariableCollection('Spacing') spacing.renameMode(spacing.modes[0].modeId, 'Default') // was 'Mode 1'
CSS variable names must not contain spaces
When constructing a var(--name) string from a Figma variable name, replace BOTH slashes AND spaces with hyphens and convert to lowercase.
// WRONG — only replacing slashes leaves spaces like 'var(--color-bg-brand secondary hover)' v.setVariableCodeSyntax('WEB', var(--${figmaName.replace(/\//g, '-').toLowerCase()}))
// CORRECT — replace all whitespace and slashes in one pass v.setVariableCodeSyntax('WEB', var(--${figmaName.replace(/[\s\/]+/g, '-').toLowerCase()}))
Best practice: Preserve the original CSS variable name from the source token file rather than deriving it from the Figma name.
// Preferred — use the source CSS name directly v.setVariableCodeSyntax('WEB', var(${token.cssVar})) // e.g. '--color-bg-brand-secondary-hover'
detachInstance() invalidates ancestor node IDs
When detachInstance() is called on a nested instance inside a library component instance, the parent instance may also get implicitly detached (converted from INSTANCE to FRAME with a new ID). Any previously cached ID for the parent becomes invalid.
// WRONG — using cached parent ID after child detach
const parentId = parentInstance.id;
nestedChild.detachInstance();
const parent = await figma.getNodeByIdAsync(parentId); // null! ID changed.
// CORRECT — re-discover by traversal from a stable (non-instance) frame
const stableFrame = await figma.getNodeByIdAsync(manualFrameId);
nestedChild.detachInstance();
const parent = stableFrame.findOne(n => n.name === "ParentName");If detaching multiple nested instances across siblings, do it in a single use_figma call — discover all targets by traversal before any detachment mutates the tree.
api-reference.md: mcp_server
common-patterns.md: mcp_server
component-patterns.md: mcp_server
effect-style-patterns.md: mcp_server
gotchas.md: mcp_server
plugin-api-patterns.md: mcp_server
plugin-api-standalone.d.ts: mcp_server
plugin-api-standalone.index.md: mcp_server
text-style-patterns.md: mcp_server
validation-and-recovery.md: mcp_server
variable-patterns.md: mcp_server
working-with-design-systems: mcp_server
Plugin API Patterns
Part of the use_figma skill. Quick reference for common Figma Plugin API operations.
Contents
- Execution Basics
- Creating Nodes
- Fills and Strokes
- Auto Layout
- Effects
- Opacity and Blend Modes
- Corner Radius and Clipping
- Grouping and Organization
- Components and Variants
- Styles
- Cloning, Finding Nodes, and Grids
- Constraints and Viewport
Execution Basics
Page Context
Page context resets between use_figma calls — figma.currentPage always starts on the first page. Use await figma.setCurrentPageAsync(page) at the start of each invocation to switch to the correct page.
const targetPage = figma.root.children.find(p => p.name === "My Page");
await figma.setCurrentPageAsync(targetPage);
// targetPage.children is now populatedClosing the Plugin
Every execution must call figma.closePlugin() on success and figma.closePluginWithFailure() on error:
figma.closePlugin("Success message describing what was done");
figma.closePluginWithFailure("Description of what went wrong");figma.notify() does not exist. Return all information via the close message string.
Working Incrementally
Don't build an entire screen in one call. Break work into small steps: 1. Create tokens/variables 2. Create text styles 3. Build individual components 4. Compose sections 5. Assemble screens
Verify structure with get_metadata between steps. Use get_screenshot after each major creation milestone to catch visual problems early.
Creating Nodes
Frames
const frame = figma.createFrame();
frame.name = "Container";
frame.resize(1440, 900);
frame.x = 0;
frame.y = 0;
frame.fills = [{ type: "SOLID", color: { r: 0.98, g: 0.98, b: 0.99 } }];Text
// MUST load font before any text operations
await figma.loadFontAsync({ family: "Inter", style: "Regular" });
const text = figma.createText();
text.fontName = { family: "Inter", style: "Regular" };
text.fontSize = 16;
text.lineHeight = { value: 24, unit: "PIXELS" };
text.letterSpacing = { value: 0, unit: "PERCENT" };
text.characters = "Hello World";
text.fills = [{ type: "SOLID", color: { r: 0.1, g: 0.1, b: 0.12 } }];Rectangles
const rect = figma.createRectangle();
rect.name = "Background";
rect.resize(400, 300);
rect.cornerRadius = 12;
rect.fills = [{ type: "SOLID", color: { r: 0.95, g: 0.95, b: 0.96 } }];Ellipses
const circle = figma.createEllipse();
circle.name = "Avatar Circle";
circle.resize(48, 48);
circle.fills = [{ type: "SOLID", color: { r: 0.85, g: 0.87, b: 0.90 } }];Lines
const line = figma.createLine();
line.name = "Divider";
line.resize(400, 0);
line.strokes = [{ type: "SOLID", color: { r: 0, g: 0, b: 0 }, opacity: 0.08 }];
line.strokeWeight = 1;SVG Import
const svgString = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12h14M12 5l7 7-7 7" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
const node = figma.createNodeFromSvg(svgString);
node.name = "Icon/Arrow Right";
node.resize(24, 24);Fills & Strokes
Solid Fill
node.fills = [{ type: "SOLID", color: { r: 0.2, g: 0.2, b: 0.25 } }];Fill with Opacity
node.fills = [{ type: "SOLID", color: { r: 0.2, g: 0.2, b: 0.25 }, opacity: 0.5 }];No Fill (Transparent)
node.fills = [];Linear Gradient
node.fills = [{
type: "GRADIENT_LINEAR",
gradientStops: [
{ color: { r: 0.2, g: 0.36, b: 0.96, a: 1 }, position: 0 },
{ color: { r: 0.56, g: 0.24, b: 0.88, a: 1 }, position: 1 }
],
gradientTransform: [[1, 0, 0], [0, 1, 0]]
}];Strokes
node.strokes = [{ type: "SOLID", color: { r: 0.85, g: 0.85, b: 0.87 } }];
node.strokeWeight = 1;
node.strokeAlign = "INSIDE"; // "CENTER", "OUTSIDE"Multiple Fills (Layered)
node.fills = [
{ type: "SOLID", color: { r: 0.95, g: 0.95, b: 0.96 } },
{ type: "SOLID", color: { r: 0.2, g: 0.36, b: 0.96 }, opacity: 0.05 }
];Auto Layout
Setting Up Auto Layout
const frame = figma.createFrame();
frame.layoutMode = "VERTICAL"; // or "HORIZONTAL"
frame.primaryAxisSizingMode = "AUTO"; // Hug main axis
frame.counterAxisSizingMode = "FIXED"; // Fixed cross axis
frame.resize(360, 1); // Width fixed, height auto
frame.itemSpacing = 16; // Gap between children
frame.paddingTop = 24;
frame.paddingBottom = 24;
frame.paddingLeft = 24;
frame.paddingRight = 24;Alignment
// Main axis (direction of layout)
frame.primaryAxisAlignItems = "MIN"; // Start
frame.primaryAxisAlignItems = "CENTER"; // Center
frame.primaryAxisAlignItems = "MAX"; // End
frame.primaryAxisAlignItems = "SPACE_BETWEEN"; // Distribute
// Cross axis
frame.counterAxisAlignItems = "MIN"; // Start
frame.counterAxisAlignItems = "CENTER"; // Center
frame.counterAxisAlignItems = "MAX"; // End
// NOTE: 'STRETCH' is NOT valid — use 'MIN' + child.layoutSizingX = 'FILL'Child Sizing
// IMPORTANT: FILL can only be set AFTER the child is appended to an auto-layout parent
parent.appendChild(child)
child.layoutSizingHorizontal = "FILL"; // Stretch to parent
child.layoutSizingHorizontal = "HUG"; // Shrink to content
child.layoutSizingHorizontal = "FIXED"; // Manual width
child.layoutSizingVertical = "FILL";
child.layoutSizingVertical = "HUG";
child.layoutSizingVertical = "FIXED";Wrapping (Grid-like Layout)
frame.layoutMode = "HORIZONTAL";
frame.layoutWrap = "WRAP";
frame.itemSpacing = 24; // Horizontal gap
frame.counterAxisSpacing = 24; // Vertical gap (between rows)Absolute Positioning Within Auto Layout
child.layoutPositioning = "ABSOLUTE";
child.constraints = { horizontal: "MAX", vertical: "MIN" }; // Top-right
child.x = parentWidth - childWidth - 8;
child.y = 8;Effects
Drop Shadow
node.effects = [{
type: "DROP_SHADOW",
color: { r: 0, g: 0, b: 0, a: 0.08 },
offset: { x: 0, y: 4 },
radius: 16,
spread: -2,
visible: true,
blendMode: "NORMAL"
}];Inner Shadow
node.effects = [{
type: "INNER_SHADOW",
color: { r: 0, g: 0, b: 0, a: 0.05 },
offset: { x: 0, y: 1 },
radius: 2,
spread: 0,
visible: true,
blendMode: "NORMAL"
}];Background Blur
node.effects = [{
type: "BACKGROUND_BLUR",
radius: 16,
visible: true
}];Layer Blur
node.effects = [{
type: "LAYER_BLUR",
radius: 8,
visible: true
}];Multiple Effects
node.effects = [
{ type: "DROP_SHADOW", color: { r: 0, g: 0, b: 0, a: 0.04 }, offset: { x: 0, y: 1 }, radius: 3, spread: 0, visible: true, blendMode: "NORMAL" },
{ type: "DROP_SHADOW", color: { r: 0, g: 0, b: 0, a: 0.06 }, offset: { x: 0, y: 8 }, radius: 24, spread: -4, visible: true, blendMode: "NORMAL" }
];Opacity & Blend Modes
node.opacity = 0.5;
node.blendMode = "NORMAL"; // "MULTIPLY", "SCREEN", "OVERLAY", "DARKEN", "LIGHTEN", etc.Corner Radius
// Uniform
node.cornerRadius = 12;
// Per-corner
node.topLeftRadius = 12;
node.topRightRadius = 12;
node.bottomLeftRadius = 0;
node.bottomRightRadius = 0;Clipping
frame.clipsContent = true; // Children clipped to frame boundsGrouping & Organization
Groups
const group = figma.group([node1, node2, node3], figma.currentPage);
group.name = "Grouped Elements";Sections
const section = figma.createSection();
section.name = "My Section";
section.resizeWithoutConstraints(800, 600);
section.x = 0;
section.y = 0;
// IMPORTANT: Sections don't auto-resize — always resize after adding contentAppending Children
parentFrame.appendChild(childNode);
// Insert at a specific index
parentFrame.insertChild(0, childNode); // Insert at beginningComponents & Variants
Create Component
const component = figma.createComponent();
component.name = "Button/Primary";
component.description = "Primary action button.";Create Instance
const instance = component.createInstance();
instance.x = 200;
instance.y = 100;Import Components by Key (Team Libraries)
These methods import components from team libraries (not the same file). For components in the current file, use figma.getNodeByIdAsync() or findOne()/findAll().
// Import a published component from a team library by its key
const comp = await figma.importComponentByKeyAsync(componentKey)
const instance = comp.createInstance()
// Import a published component set from a team library by its key
const set = await figma.importComponentSetByKeyAsync(componentSetKey)
const variant = set.defaultVariant
const variantInstance = variant.createInstance()Combine as Variants
// IMPORTANT: Pass ComponentNodes (not frames)
const componentSet = figma.combineAsVariants(
[variantA, variantB, variantC],
figma.currentPage
);
componentSet.name = "Button";
componentSet.description = "Button component with multiple variants.";
// CRITICAL: Layout variants in a grid after combining (they stack at 0,0)
let maxX = 0, maxY = 0;
componentSet.children.forEach((child, i) => {
child.x = (i % numCols) * colWidth;
child.y = Math.floor(i / numCols) * rowHeight;
});
for (const child of componentSet.children) {
maxX = Math.max(maxX, child.x + child.width);
maxY = Math.max(maxY, child.y + child.height);
}
componentSet.resizeWithoutConstraints(maxX + 40, maxY + 40);Component Properties
// addComponentProperty returns a STRING key — capture it!
const labelKey = component.addComponentProperty("label", "TEXT", "Button");
const showIconKey = component.addComponentProperty("showIcon", "BOOLEAN", true);
const iconSlotKey = component.addComponentProperty("iconSlot", "INSTANCE_SWAP", defaultIconId);
// MUST link properties to child nodes via componentPropertyReferences
labelNode.componentPropertyReferences = { characters: labelKey };
iconInstance.componentPropertyReferences = {
visible: showIconKey,
mainComponent: iconSlotKey
};Styles
Text Style
await figma.loadFontAsync({ family: "Inter", style: "Regular" });
const style = figma.createTextStyle();
style.name = "Body/Default";
style.fontName = { family: "Inter", style: "Regular" };
style.fontSize = 16;
style.lineHeight = { value: 24, unit: "PIXELS" };
style.letterSpacing = { value: 0, unit: "PERCENT" };
// Apply to a text node
textNode.textStyleId = style.id;Effect Style
const shadowStyle = figma.createEffectStyle();
shadowStyle.name = "Shadow/Subtle";
shadowStyle.effects = [{
type: "DROP_SHADOW",
color: { r: 0, g: 0, b: 0, a: 0.06 },
offset: { x: 0, y: 2 },
radius: 8,
spread: 0,
visible: true,
blendMode: "NORMAL"
}];
// Apply to a node
frame.effectStyleId = shadowStyle.id;Cloning & Duplication
const clone = originalNode.clone();
clone.x = originalNode.x + originalNode.width + 40;
clone.name = "Copy of " + originalNode.name;Finding Nodes
// Find by name on current page
const node = figma.currentPage.findOne(n => n.name === "My Frame");
// Find all by type
const allTexts = figma.currentPage.findAll(n => n.type === "TEXT");
// Find all by name pattern
const allButtons = figma.currentPage.findAll(n => n.name.startsWith("Button/"));Layout Grids
frame.layoutGrids = [
{
pattern: "COLUMNS",
alignment: "STRETCH",
count: 12,
gutterSize: 24,
offset: 80,
visible: true
}
];Constraints (Non-Auto-Layout Frames)
child.constraints = {
horizontal: "LEFT_RIGHT", // LEFT, RIGHT, CENTER, LEFT_RIGHT, SCALE
vertical: "TOP" // TOP, BOTTOM, CENTER, TOP_BOTTOM, SCALE
};Viewport & Zoom
// Zoom to fit specific nodes
figma.viewport.scrollAndZoomIntoView([frame1, frame2]);Text Style API Patterns
Part of the use_figma skill. How to create, apply, and inspect text styles using the Plugin API.
>
For design system context (when to create text styles, how they relate to tokens, headless limitations), see wwds-text-styles.
Contents
- Listing Text Styles
- Creating a Text Style
- Probing Font Styles
- Creating a Type Ramp (Multi-Step)
- Applying Text Styles to Nodes
Listing Text Styles
/**
* Lists all local text styles with their key properties.
*
* @returns {Promise<Array<{id: string, name: string, key: string, fontSize: number, fontName: FontName, lineHeight: LineHeight, letterSpacing: LetterSpacing}>>}
*/
async function listTextStyles() {
const styles = await figma.getLocalTextStylesAsync();
return styles.map(s => ({
id: s.id,
name: s.name,
key: s.key,
fontSize: s.fontSize,
fontName: s.fontName,
lineHeight: s.lineHeight,
letterSpacing: s.letterSpacing
}));
}Full runnable script:
(async () => {
try {
const results = await listTextStyles();
figma.closePlugin(JSON.stringify(results));
} catch(e) { figma.closePluginWithFailure(e.toString()); }
})()Creating a Text Style
Font MUST be loaded before setting fontName. lineHeight and letterSpacing must be {value, unit} objects — bare numbers throw.
/**
* Creates a text style with all typographic properties set.
* Font MUST be loaded before calling.
*
* @param {string} name - Slash-delimited name, e.g. "body/base"
* @param {{ family: string, style: string }} fontName
* @param {number} fontSize - In pixels
* @param {{ value: number, unit: 'PIXELS' | 'PERCENT' } | { unit: 'AUTO' }} lineHeight
* @param {{ value: number, unit: 'PIXELS' | 'PERCENT' }} [letterSpacing]
* @param {string} [description] - e.g. the CSS variable name "CSS: var(--font-body-base)"
* @returns {TextStyle}
*/
function createTextStyleFull(name, fontName, fontSize, lineHeight, letterSpacing, description) {
const style = figma.createTextStyle();
style.name = name;
style.fontName = fontName;
style.fontSize = fontSize;
style.lineHeight = lineHeight; // { unit: 'AUTO' } | { value, unit: 'PIXELS'|'PERCENT' }
if (letterSpacing) style.letterSpacing = letterSpacing;
if (description) style.description = description;
return style;
}Probing Font Styles
Font style names vary per provider and per file ("SemiBold" vs "Semi Bold"). Always probe before hardcoding:
/**
* Probes available font styles for a given family.
* Useful when font style names are unknown (e.g. "SemiBold" vs "Semi Bold").
*
* @param {string} family - Font family name, e.g. "Inter"
* @param {string[]} stylesToTest - Candidate style names to probe
* @returns {Promise<string[]>} - Style names that loaded successfully
*/
async function probeAvailableFontStyles(family, stylesToTest) {
const available = [];
for (const style of stylesToTest) {
try {
await figma.loadFontAsync({ family, style });
available.push(style);
} catch (_) {}
}
return available;
}Creating a Type Ramp (Multi-Step)
Handles font loading, deduplication, and idempotency. Each entry: [name, fontFamily, fontStyle, fontSize_px, lineHeight, cssVar].
HEADLESS NOTE: setBoundVariable on TextStyle is not supported in use_figma. This function sets raw values. To bind variables, do it interactively in Figma after creation.
/**
* Creates a full type ramp from a token definition array.
* Handles font loading, deduplication, and idempotency.
*
* Each entry: [name, fontFamily, fontStyle, fontSize_px, lineHeight, cssVar]
* - lineHeight: { unit: 'AUTO' } or { value: number, unit: 'PIXELS' | 'PERCENT' }
*
* @param {Array} defs - Array of [name, fontFamily, fontStyle, fontSize, lineHeight, cssVar] tuples
* @returns {Promise<{ created: string[], skipped: string[] }>}
*/
async function createTypeRamp(defs) {
const uniqueFonts = new Set();
for (const [, family, style] of defs) {
uniqueFonts.add(JSON.stringify({ family, style }));
}
await Promise.all(
[...uniqueFonts].map(f => figma.loadFontAsync(JSON.parse(f)))
);
const existing = new Set(
(await figma.getLocalTextStylesAsync()).map(s => s.name)
);
const created = [];
const skipped = [];
for (const [name, family, style, fontSize, lineHeight, cssVar] of defs) {
if (existing.has(name)) {
skipped.push(name);
continue;
}
const ts = figma.createTextStyle();
ts.name = name;
ts.fontName = { family, style };
ts.fontSize = fontSize;
ts.lineHeight = lineHeight ?? { unit: 'AUTO' };
if (cssVar) ts.description = `CSS: var(${cssVar})`;
created.push(name);
}
return { created, skipped };
}Full runnable script:
(async () => {
try {
const defs = [
['heading/xl', 'Inter', 'Bold', 48, { unit: 'PIXELS', value: 56 }, '--font-heading-xl'],
['heading/lg', 'Inter', 'Bold', 36, { unit: 'PIXELS', value: 44 }, '--font-heading-lg'],
['body/base', 'Inter', 'Regular', 16, { unit: 'AUTO' }, '--font-body-base'],
['body/sm', 'Inter', 'Regular', 14, { unit: 'AUTO' }, '--font-body-sm'],
['code/base', 'Roboto Mono', 'Regular', 14, { unit: 'AUTO' }, '--font-code-base'],
];
const result = await createTypeRamp(defs);
figma.closePlugin(JSON.stringify(result));
} catch(e) { figma.closePluginWithFailure(e.toString()); }
})()Applying Text Styles to Nodes
/**
* Applies a text style to all TEXT nodes on the current page that match a given name pattern.
*
* @param {string} styleId - The ID of a TextStyle.
* @param {string} nodeNamePattern - Substring match against node names.
* @returns {Promise<number>} - Number of nodes the style was applied to.
*/
async function applyTextStyleToMatchingNodes(styleId, nodeNamePattern) {
const textNodes = figma.currentPage.findAllWithCriteria({ types: ['TEXT'] });
let applied = 0;
for (const node of textNodes) {
if (node.name.includes(nodeNamePattern)) {
await node.setTextStyleIdAsync(styleId);
applied++;
}
}
return applied;
}Full runnable script:
(async () => {
try {
const applied = await applyTextStyleToMatchingNodes('STYLE_ID', 'Heading');
figma.closePlugin(JSON.stringify({ applied }));
} catch(e) { figma.closePluginWithFailure(e.toString()); }
})()Validation Workflow & Error Recovery
Part of the use_figma skill. How to debug, validate, and recover from errors.
Contents
get_metadatavsget_screenshot- Error Recovery After Failed
use_figma - Cleanup Pattern
- Recommended Workflow
get_metadata vs get_screenshot
After each use_figma call, validate results using the right tool for the job. Do NOT reach for get_screenshot every time — it is expensive and should be reserved for visual checks.
get_metadata — Use for intermediate validation (preferred)
get_metadata returns an XML tree of node IDs, types, names, positions, and sizes. Use it to confirm:
- Structure & hierarchy: correct parent-child relationships, component nesting, section contents
- Node counts: expected number of variants created, children present
- Naming: variant property names follow the
property=valueconvention - Positioning & alignment: x/y coordinates, width/height values match expectations
- Layout properties: auto-layout direction, sizing mode, padding, spacing
- Component set membership: all expected variants are inside the ComponentSet
Example: After creating a ComponentSet with 120 variants, call get_metadata on the
ComponentSet node to verify all 120 children exist with correct names, sizes, and positions
— without waiting for a full render.When to use `get_metadata`:
- After creating/modifying nodes — to verify structure, counts, and names
- After layout operations — to verify positions and dimensions
- After combining variants — to confirm all components are in the ComponentSet
- After binding variables — to verify node properties (use use_figma to read bound variables if needed)
- Between multi-step workflows — to confirm step N succeeded before starting step N+1
get_screenshot — Use after each major creation milestone
get_screenshot renders a pixel-accurate image. It is the only way to verify visual correctness (colors, typography rendering, effects, variable mode resolution). It is slower and produces large responses, so don't call it after every single use_figma — but do call it after each major milestone to catch visual problems early.
When to use `get_screenshot`:
- After creating a component set — verify variants look correct, grid is readable, nothing is collapsed or overlapping
- After composing a layout — verify overall structure and spacing
- After binding variables/modes — verify colors and tokens resolved correctly
- After any fix or recovery — verify the fix didn't introduce new visual issues
- Before reporting results to the user — final visual proof
What to look for in screenshots — these are the most commonly missed issues:
- Cropped/clipped text — line heights or frame sizing cutting off descenders, ascenders, or entire lines
- Overlapping content — elements stacking on top of each other due to incorrect sizing or missing auto-layout
- Placeholder text still showing ("Title", "Heading", "Button") instead of actual content
CRITICAL: Error Recovery After Failed use_figma
THIS IS NOT OPTIONAL. Every use_figma error MUST trigger the recovery steps below. Skipping these steps leaves orphaned nodes in the file that will cause duplicates and inconsistencies on retry.Scripts can partially execute before hitting an error. A failed use_figma does NOT roll back — nodes created before the error line persist in the file. This leaves the file in an inconsistent, partially-modified state.
Mandatory recovery steps when `use_figma` returns an error (DO NOT SKIP): 1. STOP — do NOT immediately fix the code and retry. The file has partial state that must be inspected first. 2. Immediately call `get_metadata` on the parent node (section, page, or ComponentSet) to see what was partially created. 3. If `get_metadata` doesn't make the damage clear (e.g. positions look fine but visual state is uncertain), call get_screenshot to assess visual damage. 4. Write a cleanup script to remove orphaned/incomplete nodes before retrying. Use page.findChildren() to locate stray nodes. 5. Only after cleanup is confirmed, fix the original script and retry. 6. Never retry the failed script blindly — the partial state means a retry will create duplicates or hit new errors.
Example: A script creating 8 components fails on component #5.
Components 1-4 exist on the page. A naive retry creates components 1-8 again,
leaving 12 components total (4 orphaned duplicates). Always clean up first.Cleanup Pattern
// Cleanup pattern: find and remove orphaned nodes from a failed run
(async () => {
try {
const page = figma.currentPage;
// Find orphaned components that weren't combined into a ComponentSet
const orphans = page.findChildren(n =>
n.type === 'COMPONENT' && n.name.includes('variant=')
);
for (const orphan of orphans) orphan.remove();
figma.closePlugin('Cleaned up ' + orphans.length + ' orphaned nodes');
} catch(e) { figma.closePluginWithFailure(e.toString()); }
})()Recommended Workflow
1. use_figma → Create/modify nodes
2. get_metadata → Verify structure, counts, names, positions (fast, cheap)
3. use_figma → Fix any structural issues found
4. get_metadata → Re-verify fixes
5. ... repeat as needed ...
6. get_screenshot → Visual check after each major milestone
⚠️ ON ERROR at any step:
a. get_metadata → Inspect partial state (always do this first)
b. get_screenshot → Only if metadata doesn't make the damage clear
c. use_figma → Clean up orphaned/incomplete nodes
d. THEN retry the failed operationVariable & Token API Patterns
Part of the use_figma skill. How to correctly create, bind, scope, and alias variables using the Plugin API.
>
For design system context (aliasing strategy, mode decisions, code syntax philosophy, grouping conventions), see wwds-variables.
Contents
- Creating Variable Collections and Modes
- Creating Variables (All Types)
- Binding Variables to Node Properties
- Variable Scopes: What They Are and How to Set Them
- Variable Aliasing (VARIABLE_ALIAS)
- Code Syntax (setVariableCodeSyntax)
- Discovering Existing Variables in the File
- Effect Styles (For Shadows)
Creating Variable Collections and Modes
const collection = figma.variables.createVariableCollection("MyCollection");
// A new collection starts with 1 mode named "Mode 1" — always rename it
collection.renameMode(collection.modes[0].modeId, "Light");
// Add additional modes (returns the new modeId)
const darkModeId = collection.addMode("Dark");
const lightModeId = collection.modes[0].modeId;Mode limits are plan-dependent: Free = 1 mode, Professional = up to 4, Organization/Enterprise = 40+. If you need many modes, split across multiple collections.
Creating Variables (All Types)
figma.variables.createVariable(name, collection, resolvedType) — the second argument accepts a collection object or ID string (object preferred).
// COLOR — values use {r, g, b, a} (all 0–1 range, includes alpha)
const colorVar = figma.variables.createVariable("my-color", collection, "COLOR");
colorVar.setValueForMode(modeId, { r: 0.2, g: 0.36, b: 0.96, a: 1 });
// FLOAT — for spacing, radii, sizing, numeric values
const floatVar = figma.variables.createVariable("my-spacing", collection, "FLOAT");
floatVar.setValueForMode(modeId, 16);
// STRING — for font families, font style names, any text value
const stringVar = figma.variables.createVariable("my-font", collection, "STRING");
stringVar.setValueForMode(modeId, "Inter");
// BOOLEAN
const boolVar = figma.variables.createVariable("my-flag", collection, "BOOLEAN");
boolVar.setValueForMode(modeId, true);Note: Paint colors use {r, g, b} (no alpha), but COLOR variable values use {r, g, b, a} (with alpha). Don't mix them up.
Binding Variables to Node Properties
Color Bindings (Fills, Strokes)
setBoundVariableForPaint returns a NEW paint — you must capture the return value:
// Create a base paint, bind the variable, assign the result
const basePaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } };
const boundPaint = figma.variables.setBoundVariableForPaint(basePaint, "color", colorVar);
node.fills = [boundPaint];
// Only SOLID paints support color variable binding — gradients/images will throwNumeric Bindings (Spacing, Radii, Sizing)
setBoundVariable binds FLOAT/STRING/BOOLEAN variables to node properties:
// Padding
node.setBoundVariable("paddingTop", spacingVar);
node.setBoundVariable("paddingBottom", spacingVar);
node.setBoundVariable("paddingLeft", spacingVar);
node.setBoundVariable("paddingRight", spacingVar);
// Gap
node.setBoundVariable("itemSpacing", gapVar);
node.setBoundVariable("counterAxisSpacing", gapVar);
// Corner radius — use individual corners, NOT cornerRadius
node.setBoundVariable("topLeftRadius", radiusVar);
node.setBoundVariable("topRightRadius", radiusVar);
node.setBoundVariable("bottomLeftRadius", radiusVar);
node.setBoundVariable("bottomRightRadius", radiusVar);
// Size
node.setBoundVariable("width", sizeVar);
node.setBoundVariable("height", sizeVar);
node.setBoundVariable("minWidth", sizeVar);
node.setBoundVariable("maxWidth", sizeVar);
// Other
node.setBoundVariable("opacity", opacityVar);
node.setBoundVariable("strokeWeight", strokeVar);Not bindable via setBoundVariable: fontSize, fontWeight, lineHeight — set these directly on text nodes.
Effect Bindings
const effectCopy = JSON.parse(JSON.stringify(node.effects[0]));
const newEffect = figma.variables.setBoundVariableForEffect(effectCopy, "color", colorVar);
// ⚠️ Returns a NEW effect — must capture return value!
node.effects = [newEffect];
// Valid fields: "color" (COLOR), "radius" | "spread" | "offsetX" | "offsetY" (FLOAT)Applying a Mode to a Frame
// All bound children of this frame will resolve to the specified mode's values
frame.setExplicitVariableModeForCollection(collection.id, modeId);Without this, all nodes use the collection's default (first) mode.
Variable Scopes: What They Are and How to Set Them
variable.scopes controls which Figma property pickers show the variable. The default is ["ALL_SCOPES"] which shows it everywhere — this is almost never what you want.
variable.scopes = ["FRAME_FILL", "SHAPE_FILL"]; // only fill pickers
variable.scopes = ["TEXT_FILL"]; // only text color picker
variable.scopes = ["GAP"]; // only gap/spacing pickers
variable.scopes = ["CORNER_RADIUS"]; // only radius pickers
variable.scopes = []; // hidden from all pickersAll valid scope values: ALL_SCOPES, TEXT_CONTENT, CORNER_RADIUS, WIDTH_HEIGHT, GAP, ALL_FILLS, FRAME_FILL, SHAPE_FILL, TEXT_FILL, STROKE_COLOR, STROKE_FLOAT, EFFECT_FLOAT, EFFECT_COLOR, OPACITY, FONT_FAMILY, FONT_STYLE, FONT_WEIGHT, FONT_SIZE, LINE_HEIGHT, LETTER_SPACING, PARAGRAPH_SPACING, PARAGRAPH_INDENT
Always check the existing file's scope patterns before creating variables — match whatever convention is already in use. See "Discovering Existing Variables" below.
Variable Aliasing (VARIABLE_ALIAS)
A variable's value can reference another variable via alias. This is how semantic tokens reference primitive tokens:
// Set a variable's value as an alias to another variable
semanticVar.setValueForMode(modeId, {
type: 'VARIABLE_ALIAS',
id: primitiveVar.id
});When the primitive changes, the semantic variable updates automatically across all modes.
Code Syntax (setVariableCodeSyntax)
Links a Figma variable back to its code counterpart. Call once per platform:
variable.setVariableCodeSyntax('WEB', 'var(--color-bg-default)');
variable.setVariableCodeSyntax('ANDROID', 'colorBgDefault');
variable.setVariableCodeSyntax('iOS', 'Color.bgDefault');
// Read back: variable.codeSyntax → { WEB: '...', ANDROID: '...', iOS: '...' }When deriving CSS names from Figma names, replace both slashes AND spaces with hyphens:
// WRONG — leaves spaces in CSS variable name
`var(--${figmaName.replace(/\//g, '-').toLowerCase()})`
// CORRECT — replace all whitespace and slashes
`var(--${figmaName.replace(/[\s\/]+/g, '-').toLowerCase()})`
// BEST — use the original CSS variable name from the source, not a derived one
`var(${token.cssVar})`Discovering Existing Variables in the File
Always inspect the file's existing variables before creating new ones. Different files use different naming conventions, scope patterns, and collection structures. Match what's already there.
List collections with mode info
(async () => {
try {
const collections = figma.variables.getLocalVariableCollections();
const results = collections.map(c => ({
name: c.name,
id: c.id,
varCount: c.variableIds.length,
modes: c.modes.map(m => ({ name: m.name, id: m.modeId }))
}));
figma.closePlugin(JSON.stringify(results));
} catch(e) { figma.closePluginWithFailure(e.toString()); }
})()Inspect scope patterns used in existing variables
(async () => {
try {
const collections = figma.variables.getLocalVariableCollections();
const scopeGroups = {};
for (const c of collections) {
for (const id of c.variableIds) {
const v = figma.variables.getVariableById(id);
const key = JSON.stringify(v.scopes);
if (!scopeGroups[key]) scopeGroups[key] = [];
scopeGroups[key].push(v.name);
}
}
figma.closePlugin(JSON.stringify(scopeGroups));
} catch(e) { figma.closePluginWithFailure(e.toString()); }
})()Build a name→variable lookup for reuse
const varByName = {};
for (const v of figma.variables.getLocalVariables()) {
varByName[v.name] = v;
}
// Bind to existing variable by name — no hex values needed
function bindFill(node, varName) {
const v = varByName[varName];
if (!v) throw new Error(`Variable not found: ${varName}`);
const paint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', v
);
node.fills = [paint];
}Only create new variables for tokens that have no match in the file. After building the lookup, compare against the needed tokens and create variables only for the delta.
Listing Collections with Full Variable Details
The async API returns richer data including code syntax and scopes per variable:
/**
* Lists all local variable collections defined in the current Figma file,
* including metadata for their modes and variables.
*
* @returns {Promise<Array<{
* name: string,
* id: string,
* modes: Array<[name: string, modeId: string]>,
* variables: Array<[name: string, id: string, codeSyntax: object, scopes: string[]]>
* }>>}
*/
async function listVariableCollectionsAndVariables() {
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const results = [];
for (const collection of collections) {
const vars = [];
for (const id of collection.variableIds) {
const v = await figma.variables.getVariableByIdAsync(id);
vars.push([v.name, v.id, v.codeSyntax, v.scopes]);
}
results.push({
name: collection.name,
id: collection.id,
modes: collection.modes.map(m => [m.name, m.modeId]),
variables: vars
});
}
return results;
}Full runnable script:
(async () => {
try {
const results = await listVariableCollectionsAndVariables();
figma.closePlugin(JSON.stringify(results));
} catch(e) { figma.closePluginWithFailure(e.toString()); }
})()Setting and Removing Code Syntax
Must be executed in the file the variable is defined in:
/**
* Set the code syntax for a variable for a specific platform.
*
* @param {string} variableId
* @param {'WEB'|'ANDROID'|'iOS'} platform
* @param {string} syntax
*/
async function setVariableCodeSyntax(variableId, platform, syntax) {
const variable = await figma.variables.getVariableByIdAsync(variableId);
variable.setVariableCodeSyntax(platform, syntax);
}
/**
* Remove code syntax for a variable for one or more platforms.
*
* @param {string} variableId
* @param {Array<'WEB'|'ANDROID'|'iOS'>} platforms — defaults to all three
*/
async function removeVariableCodeSyntax(variableId, platforms = ["WEB", "ANDROID", "iOS"]) {
const variable = await figma.variables.getVariableByIdAsync(variableId);
for (const platform of platforms) {
variable.removeVariableCodeSyntax(platform);
}
}
/**
* Set a value for a variable in a specific mode.
* For aliases, value must be: { type: 'VARIABLE_ALIAS', id: '<variableId>' }
*
* @param {string} variableId
* @param {string} modeId
* @param {string|number|boolean|RGB|RGBA|{type: 'VARIABLE_ALIAS', id: string}} value
*/
async function setVariableValueForMode(variableId, modeId, value) {
const variable = await figma.variables.getVariableByIdAsync(variableId);
variable.setValueForMode(modeId, value);
}Effect Styles (For Shadows)
Shadows can't be stored as variables. Use effect styles. For comprehensive patterns, see effect-style-patterns.md.
const shadow = figma.createEffectStyle();
shadow.name = "Shadow/Subtle";
shadow.effects = [{
type: "DROP_SHADOW",
color: { r: 0, g: 0, b: 0, a: 0.06 },
offset: { x: 0, y: 2 },
radius: 8,
spread: 0,
visible: true,
blendMode: "NORMAL"
}];
// Apply to a node
frame.effectStyleId = shadow.id;wwds-components--creating.md: mcp_server
wwds-components--using.md: mcp_server
wwds-components.md: mcp_server
wwds-effect-styles.md: mcp_server
wwds-text-styles.md: mcp_server
wwds-variables--creating.md: mcp_server
wwds-variables--using.md: mcp_server
wwds-variables.md: mcp_server
wwds.md: mcp_server
Related skills
FAQ
What does figma-use do?
**MANDATORY prerequisite** — you MUST invoke this skill BEFORE every `use_figma` tool call. NEVER call `use_figma` directly without loading this skill first. Skipping it causes common, hard-to-debug f
When should I use figma-use?
During build integrations work for ai & agent building.
Is figma-use safe to install?
Review the Security Audits panel on this listing before production use.