
Figma Use
- 148 installs
- 64 repo stars
- Updated July 13, 2026
- thedesignproject/agent-skills
This is a copy of figma-use by figma - installs and ranking accrue to the original listing.
Use when working on frontend tasks.
About
Figma Use skill provides developer tools and automation. Complexity: intermediate.
- Figma
- Use
Figma Use by the numbers
- 148 all-time installs (skills.sh)
- +16 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedesignproject/agent-skills --skill figma-useAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 148 |
|---|---|
| repo stars | ★ 64 |
| Last updated | July 13, 2026 |
| Repository | thedesignproject/agent-skills ↗ |
What it does
Use when working on frontend tasks.
Files
use_figma — Figma Plugin API Skill
Use the use_figma tool 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 Figma MCP tools appear as deferred tools, batch-load all their schemas in a single `ToolSearch` call using the select: syntax — e.g. ToolSearch query="select:use_figma,get_screenshot,get_metadata,create_new_file". One round trip beats six.
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.
If the task involves creating or building a component in Figma (even a single component), also load figma-generate-library. It provides the component creation workflow — variable foundations, variant sets, design token bindings — that figma-use alone doesn't cover.
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. Every text edit follows the canonical recipe: load font → `await` → mutate → return affected node IDs. Skipping the load throws Cannot write to node with unloaded font "<family> <style>". The rule covers more than characters — it applies to any operation on nodes with unloaded fonts (appendChild, insertChild, setBoundVariable, setExplicitVariableModeForCollection, setValueForMode, findAll callbacks touching text). When mutating existing text, load the node's current fonts via getStyledTextSegments(['fontName']), not a hardcoded default. Inter is preloaded in most environments so other families surface this bug more often — the recipe is the same for every font. Use await figma.listAvailableFontsAsync() first if the style string is unverified. See Canonical text-edit recipe. 9. Pages load incrementally — use await figma.setCurrentPageAsync(page) to switch pages and load their content. The sync setter figma.currentPage = page does NOT work and will throw (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` is value-restricted by structural context — `FIXED` always works, `HUG` and `FILL` do not. 'HUG' is valid only on an auto-layout frame itself OR on a TEXT child of one. 'FILL' is valid only on a child of an auto-layout frame that is also not absolute-positioned, not inside an immutable frame, and not a canvas-grid child. Practical consequence: append to an auto-layout parent FIRST, then set HUG/FILL — a newly-created or unparented node can't satisfy the rule yet. The property itself exists on every SceneNode; the error is value-rejection, not "no such property". See Gotchas. 12a. Use auto-layout for containers that hold related children. When children have a structural relationship — stacked, side-by-side, aligned, gapped, hugged — wrap them in figma.createAutoLayout(), not figma.createFrame() with absolute x/y. Absolute coordinates govern where a container sits on the canvas; auto-layout governs how its children relate inside it. Skipping the container leaves no protection against text reflow, content changes, or overlap. 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 does NOT work — it throws "Setting figma.currentPage is not supported" in use_figma. Always use the async method.
// 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 populatedCall setCurrentPageAsync at most once per use_figma invocation — fan multi-page work out in parallel
One script must switch pages at most once. Never loop over figma.root.children and switch pages inside the loop.
If the work spans multiple pages, split it into N `use_figma` calls (one per target page) and emit them in parallel — a single assistant message containing N use_figma tool-use blocks. The harness runs them concurrently; each script sets currentPage exactly once.
Explicit instruction: when fanning out, you MUST issue the N tool calls in one message. Do not send them across multiple turns. Do not await one before issuing the next. Sequential per-page calls are slower than the in-loop pattern this rule replaces and waste the entire benefit of splitting.
// AVOID — switches pages N times in one script, reloads the file each time
for (const page of figma.root.children) {
await figma.setCurrentPageAsync(page);
// ... touch this page ...
}
// PREFER — read-only discovery call to get page IDs, then in the NEXT message
// emit N parallel use_figma tool calls (one per page), each setting currentPage once.Default to parallel fan-out for any multi-page work — reads and writes alike. See gotchas.md → Set current page once per `use_figma` call for the full rationale.
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") and Slides ("slides") have different sets of available node types — most design nodes are blocked in FigJam, and FigJam-only nodes are blocked in Slides.
Tell the editor from the URL: Design = figma.com/design/..., FigJam = figma.com/board/..., Slides = figma.com/slides/.... Confirm before assuming an API is available.
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, SlideGrid, InteractiveSlideElement, Webpage.
Available in Slides mode: Rectangle, Frame, Component, Text, Ellipse, Star, Line, Vector, Polygon, BooleanOperation, Slice, Section, TextPath, Slide, SlideRow, SlideGrid, InteractiveSlideElement.
Blocked in Slides mode: Sticky, Connector, ShapeWithText, CodeBlock, Webpage, Page.
Design-only APIs (not just node types): figma.createPage() is available only in Design files (figma.com/design/...). In both FigJam (figma.com/board/...) and Slides (figma.com/slides/...) it throws TypeError: figma.createPage no such property 'createPage' on the figma global object. Do not emit figma.createPage() in FigJam or Slides workflows.
Slides note: There is no dedicated read tool for Slides files yet. Useuse_figmawith read-only scripts for inspection (see Section 6 "Inspect first" pattern), andget_screenshot/await node.screenshot()for visual context. For Slides-specific API guidance, load the figma-use-slides skill.
5. Efficient APIs — Prefer These Over Verbose Alternatives
These APIs reduce boilerplate, eliminate ordering errors, and compress token output. Always prefer them over the verbose alternatives.
node.query(selector) — CSS-like node search
Find nodes within a subtree using CSS-like selectors. Replaces verbose findAll + filter loops.
// BEFORE — verbose traversal
const texts = frame.findAll(n => n.type === 'TEXT' && n.name === 'Title')
// AFTER — one-liner with query
const texts = frame.query('TEXT[name=Title]')Selector syntax:
- Type:
FRAME,TEXT,RECTANGLE,ELLIPSE,COMPONENT,INSTANCE,SECTION(case-insensitive) - Attribute exact:
[name=Card],[visible=true],[opacity=0.5] - Attribute substring:
[name*=art](contains),[name^=Header](starts-with),[name$=Nav](ends-with) - Dot-path traversal:
[fills.0.type=SOLID],[fills.*.type=SOLID](wildcard index) - Instance matching:
[mainComponent=nodeId],[mainComponent.name=Button] - Combinators:
FRAME > TEXT(direct child),FRAME TEXT(any descendant),A + B(adjacent sibling),A ~ B(general sibling) - Pseudo-classes:
:first-child,:last-child,:nth-child(2),:not(TYPE),:is(FRAME, RECTANGLE),:where(TEXT, ELLIPSE) - Node ID:
#nodeIdor bare GUID - Comma:
TEXT, RECTANGLE(union) - Wildcard:
*(any type)
QueryResult methods:
| Method | Description |
|---|---|
.length | Number of matched nodes |
.first() | First matched node (or null) |
.last() | Last matched node (or null) |
.toArray() | Convert to regular array |
.each(fn) | Iterate with callback, returns this for chaining |
.map(fn) | Map to new array |
.filter(fn) | Filter to new QueryResult |
.values(keys) | Extract property values: .values(['name', 'x', 'y']) → [{name, x, y}, ...] |
.set(props) | Set properties on all matched nodes (see node.set() below) |
.query(selector) | Sub-query within matched nodes |
for...of | Iterable — works in for loops |
Scope: node.query() searches within that node's subtree. To search the whole page: figma.currentPage.query('...'). There is no global figma.query().
Examples:
// Recolor all text inside cards
figma.currentPage.query('FRAME[name^=Card] TEXT').set({
fills: [{type: 'SOLID', color: {r: 0.2, g: 0.2, b: 0.8}}]
})
// Get names and positions of all frames
return figma.currentPage.query('FRAME').values(['name', 'x', 'y'])
// Find the first component named "Button"
const btn = figma.currentPage.query('COMPONENT[name=Button]').first()
// Find all instances of a specific component
figma.currentPage.query(`INSTANCE[mainComponent=${compId}]`)
// Find nodes with solid fills using dot-path traversal
figma.currentPage.query('[fills.0.type=SOLID]')node.set(props) — batch property updates
Set multiple properties in one call. Returns this for chaining.
// BEFORE — one line per property
frame.opacity = 0.5
frame.cornerRadius = 8
frame.name = "Card"
// AFTER — single call
frame.set({ opacity: 0.5, cornerRadius: 8, name: "Card" })Priority key ordering: layoutMode is always applied before other properties (like width/height) regardless of object key order. This prevents the common bug where resize() behaves differently depending on whether layoutMode is set.
Width/height handling: width and height are routed through node.resize() automatically — setting { width: 200 } calls resize(200, currentHeight).
Chaining with query:
// Find all rectangles named "Divider" and update them
figma.currentPage.query('RECTANGLE[name=Divider]').set({
fills: [{type: 'SOLID', color: {r: 0.9, g: 0.9, b: 0.9}}],
cornerRadius: 2
})figma.createAutoLayout(direction?, props?) — auto-layout frames
Creates a frame with auto-layout already enabled and both axes hugging content. This is the default container whenever children have a structural relationship to each other (see Rule 12a).
// BEFORE — manual setup, easy to get ordering wrong
const frame = figma.createFrame()
frame.layoutMode = 'VERTICAL'
frame.primaryAxisSizingMode = 'AUTO'
frame.counterAxisSizingMode = 'AUTO'
frame.layoutSizingHorizontal = 'HUG'
frame.layoutSizingVertical = 'HUG'
// AFTER — one call, layout ready
const frame = figma.createAutoLayout('VERTICAL')Children can immediately use layoutSizingHorizontal/Vertical = 'FILL' after being appended — no need to set sizing modes manually.
Accepts an optional props object as the first or second argument:
figma.createAutoLayout({ name: 'Card', itemSpacing: 12 }) // HORIZONTAL + props
figma.createAutoLayout('VERTICAL', { name: 'Column', itemSpacing: 8 }) // VERTICAL + propsnode.placeholder — shimmer overlay for AI-in-progress feedback
Sets a visual shimmer overlay on a node indicating work is in progress. Always remove the shimmer when done — leftover shimmers confuse users and indicate incomplete work.
// Mark as in-progress
frame.placeholder = true
// ... build out the content ...
// MUST remove when done — never leave shimmers on finished nodes
frame.placeholder = falseWhen building complex layouts, set placeholder = true on sections before populating them, then set placeholder = false on each section as it's completed.
await node.screenshot(opts?) — inline screenshots
Capture a node as a PNG and return it inline in the response. Eliminates the need for a separate get_screenshot call.
// Take a screenshot of a frame (returned inline in the tool response)
await frame.screenshot()
// Custom scale (default auto-scales: 0.5x or capped so max dimension ≤ 1024px)
await frame.screenshot({ scale: 2 })
// Include overlapping content from sibling nodes
await frame.screenshot({ contentsOnly: false })When to use: After creating or modifying nodes, call screenshot() to visually verify the result within the same script. No need for a separate get_screenshot call.
Auto-naming: The image caption includes node metadata — "Card (300x150 at 0,60).png" — giving spatial context without parsing the image.
Default scaling: Uses 0.5x scale, but automatically caps so the largest output dimension never exceeds 1024px. Explicit { scale: N } bypasses the cap.
6. 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.
Key rules
- At most 10 logical operations per `use_figma` call. A "logical operation" is creating a node, setting its properties, and parenting it. If you need to create 20 nodes, split across 2-3 calls.
- Build top-down, starting with placeholders. Create the outer structure first with
placeholder = trueon each section, then incrementally replace placeholders with real content in subsequent calls.
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. Build the skeleton. Create the top-level structure with placeholder sections. Set placeholder = true on each section so the user sees progress. 3. Fill in sections incrementally. In each subsequent call, populate one section and set its placeholder = false when done. Take a screenshot() to verify. 4. 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. 5. Validate after each step. Use get_metadata to verify structure (counts, names, hierarchy, positions). Use await node.screenshot() inline or get_screenshot after major milestones to catch visual issues. 6. 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 |
7. 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 or a child of an auto-layout frame" / "FILL can only be set on children of auto-layout frames" / "HUG can only be set on auto-layout frames or text children of auto-layout frames" / "FILL cannot be set on absolute positioned auto-layout children" / "FILL cannot be set on canvas grid children" | Tried to assign HUG/FILL to a node whose structural context doesn't allow it (e.g. parent isn't auto-layout, ran before appendChild, non-text child trying to HUG, absolute-positioned child trying to FILL) | Make the parent auto-layout via figma.createAutoLayout(); appendChild first; reserve HUG for the auto-layout frame itself or for TEXT children; for absolute/immutable/grid children use FIXED + resize(). See gotchas.md |
"Setting figma.currentPage is not supported" | Used sync page setter (figma.currentPage = page) which does NOT work | Use await figma.setCurrentPageAsync(page) — the only way to switch pages |
| 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.
8. 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)
- [ ] Paint
colorobjects use{r, g, b}only — noafield (opacity goes at the paint level:{ type: 'SOLID', color: {...}, opacity: 0.5 }) - [ ] Fills/strokes are reassigned as new arrays (not mutated in place)
- [ ] Page switches use
await figma.setCurrentPageAsync(page)(sync setterfigma.currentPage = pagedoes NOT work) - [ ]
layoutSizingVertical/Horizontal = 'FILL'is set AFTERparent.appendChild(child) - [ ] Every text mutation follows the canonical recipe:
loadFontAsync→await→ mutatecharacters/font/size/etc. → return affected node IDs. Works for ANY font family/style, not just Inter (which only happens to be preloaded). - [ ] Style names have already been verified via
listAvailableFontsAsync()— NOT guessed from memory ("SemiBold"vs"Semi Bold"is a common footgun) - [ ] For
FONT_FAMILY-scoped variables: every value across every relevant mode is loaded beforesetBoundVariable("fontFamily", …),setValueForMode, orsetExplicitVariableModeForCollection - [ ]
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
- [ ] Containers with structurally-related children use
figma.createAutoLayout(), not absolute x/y (see Rule 12a) - [ ] 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
9. 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:
search_design_system is an option for published components. For on-canvas components, use the two-step fan-out — don't loop pages inside one script.
Step 1: one read-only use_figma to get page IDs:
return figma.root.children.map(p => ({ id: p.id, name: p.name }));Step 2: in the next assistant turn, emit one `use_figma` per page in parallel (a single message containing N tool-use blocks). Each runs:
// Read-only inspection — skip invisible instance interiors for the
// hundreds-of-times-faster findAllWithCriteria.
figma.skipInvisibleInstanceChildren = true;
const page = await figma.getNodeByIdAsync(PAGE_ID);
await figma.setCurrentPageAsync(page);
// findAllWithCriteria uses an indexed type lookup — hundreds of times faster
// than the findAll(n => n.type === '…') side-effect-in-predicate antipattern.
const matches = page.findAllWithCriteria({ types: ['COMPONENT', 'COMPONENT_SET'] });
return matches.map(n => ({ page: page.name, name: n.name, type: n.type, id: n.id }));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;10. 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 — start with the canonical text-edit recipe |
| 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 discovery via listAvailableFontsAsync, 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 |
11. 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.
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.createAutoLayout() // Frame with auto layout enabled, both axes hug — prefer over createFrame() for layout containers
figma.createAutoLayout("VERTICAL") // Same but vertical direction
figma.createComponent() // Creates a ComponentNode
figma.createText()
figma.createEllipse()
figma.createStar()
figma.createLine()
figma.createVector()
figma.createPolygon()
figma.createBooleanOperation()
figma.createSlice()
figma.createPage() // Design files ONLY (figma.com/design/...). Throws "no such property 'createPage'" in both FigJam (figma.com/board/...) and Slides (figma.com/slides/...). Child persistence is limited in use_figma.
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.getLocalVariablesAsync() or figma.variables.getVariableByIdAsync().
// 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")
// ^ must be a collection object (passing an ID string is deprecated)
// 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 (always use the Async variants — sync versions are deprecated)
await figma.variables.getVariableByIdAsync(id)
await figma.variables.getLocalVariablesAsync(resolvedType?)
await figma.variables.getVariableCollectionByIdAsync(id)
await figma.variables.getLocalVariableCollectionsAsync()
// 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(collection, modeId) // pass collection object, NOT an ID string
// Without this, all nodes use the default (first) mode of the collectionCore Properties
figma.root // DocumentNode
figma.currentPage // Current page — READ ONLY; the sync setter (figma.currentPage = page) does NOT work and throws
figma.setCurrentPageAsync(page) // Switch page and load its content (MUST await) — this is the ONLY way to change pages
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' — see Gotchas: HUG needs auto-layout frame or TEXT child; FILL needs an auto-layout-child that isn't absolute/immutable/grid
node.layoutSizingVertical = 'HUG' // 'FIXED' | 'HUG' | 'FILL' — same value rules as horizontal
// 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
`upload_assets` is the ONLY supported way to upload images into a Figma file — Design, FigJam, and Slides all share this path. Do NOT use `figma.createImage()` or `figma.createImageAsync()` from inside `use_figma`. Both are unsupported as image-upload entry points and will be removed from agent flows; use_figma has no network access (so createImageAsync(src) cannot fetch URLs) and bytes inside the script are not durable assets in the file.
The upload_assets tool is the ONLY supported way. It returns single-use upload URLs that you POST raw bytes to, and the response contains an imageHash plus placement details. Server-side commit and canvas placement happen automatically. Pass nodeId (with count: 1) to set the upload as a fill on an existing node directly, or omit nodeId to place the image on the canvas as a new layer.
upload_assets({ fileKey, count: 1, nodeId, scaleMode: 'FILL' })
→ { uploads: [{ submitUrl }], instructions: "..." }
// Then POST the image bytes to submitUrl (multipart/form-data 'file' field
// preferred — the filename becomes the layer name).Re-using an existing imageHash (not an upload)
Once an image is in the file via upload_assets, you can reference its imageHash from another node without re-uploading. This is the only legitimate use of an imageHash inside use_figma:
// Re-using an imageHash that already exists on another node in the file
node.fills = [{ type: 'IMAGE', scaleMode: 'FILL', imageHash: 'hash_from_existing_node' }]For anything originating outside the file (URLs, local files, generated bytes, screenshots) — always call upload_assets first.
Fonts
The canonical text-edit recipe is load font → `await` → mutate → return affected IDs — see gotchas.md → Canonical text-edit recipe for WRONG/CORRECT examples. The rule applies to every font, not just Inter (Inter is preloaded in most environments, which is why the bug usually surfaces with other families).
// Discover all available fonts and their exact style strings
const allFonts = await figma.listAvailableFontsAsync() // Font[] — each has { fontName: { family, style } }
const interStyles = allFonts.filter(f => f.fontName.family === "Inter")
// MUST load a font before any text property edit — for every font, not just Inter
await figma.loadFontAsync({ family: "Inter", style: "Regular" })
// Check if the file has missing fonts
figma.hasMissingFont // booleanUtilities
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
Scripts are automatically wrapped in an async IIFE with error handling. Use return to send data back:
return { nodeId: frame.id } // Return object — auto-serialized to JSON
return "success message" // Return string
// Errors are auto-captured — no try/catch or closePlugin neededNode Traversal
These properties and methods are defined on ChildrenMixin — they exist on container nodes only (DocumentNode, PageNode, FrameNode, GroupNode, ComponentNode, ComponentSetNode, InstanceNode, SectionNode, BooleanOperationNode). They do NOT exist on leaf nodes (TextNode, RectangleNode, EllipseNode, LineNode, PolygonNode, StarNode, VectorNode, SliceNode). Accessing .children on a leaf node throws TypeError: node.children: no such property 'children' on TEXT node (or RECTANGLE, etc.). The same pattern applies to many other mixin-scoped members (fills, layoutMode, x/y, text-only methods) — see Gotchas → "no such property" errors.
node.findAll(pred?) // Find all descendants matching predicate (ChildrenMixin only)
node.findOne(pred?) // Find first descendant matching predicate (ChildrenMixin only)
node.findChildren(pred?) // Find direct children matching predicate (ChildrenMixin only)
node.findChild(pred?) // Find first direct child matching predicate (ChildrenMixin only)
node.children // Direct children array (ChildrenMixin only)
node.parent // Parent node (all nodes)To safely descend an arbitrary subtree, guard with a "children" in node check or a type check before reading .children:
function walk(node) {
// ... do work on node ...
if ("children" in node) {
for (const child of node.children) walk(child);
}
}---
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.loadAllPagesAsync() | Not implemented |
figma.variables.extendLibraryCollectionByKeyAsync() | Not implemented |
figma.teamLibrary.* | Not implemented (requires the team-library backend) |
figma.getLocalComponents*() | Does not exist — unlike styles, there is no getLocalComponents() or getLocalComponentSetsAsync() (or any getLocalComponent* variant). Use page.findAllWithCriteria({ types: ['COMPONENT', 'COMPONENT_SET'] }) to locate components in the current file (avoid the slower findAll(n => n.type === '…') predicate scan). |
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
const createdNodeIds = []
const mutatedNodeIds = []
// Your code here — track every node you create or mutate
// createdNodeIds.push(newNode.id)
// mutatedNodeIds.push(existingNode.id)
return {
success: true,
createdNodeIds,
mutatedNodeIds,
// Plus any other useful data for subsequent calls
count: createdNodeIds.length
}Create a Styled Shape
// 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)
return { nodeId: rect.id }Create a Text Node
Canonical text-edit recipe: load font → await → mutate → return affected IDs. Inter is preloaded in most environments; for any other family/style you would have hit Cannot write to node with unloaded font "<family> <style>" without the load step — the recipe is identical regardless of font.
// 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)
}
// Load font BEFORE any text mutation — required for every font, not just Inter
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)
return { createdNodeIds: [text.id] }Create Frame with Auto-Layout
// 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.createAutoLayout('VERTICAL')
frame.name = "Card"
frame.primaryAxisAlignItems = 'MIN'
frame.counterAxisAlignItems = 'MIN'
frame.paddingLeft = 16
frame.paddingRight = 16
frame.paddingTop = 12
frame.paddingBottom = 12
frame.itemSpacing = 8
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)
return { nodeId: frame.id }Create Variable Collection with Multiple Modes
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 })
return {
collectionId: collection.id,
lightModeId,
darkModeId,
bgVarId: bgVar.id,
textVarId: textVar.id
}Bind Color Variable to a Fill
const variable = await figma.variables.getVariableByIdAsync("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]
return { nodeId: rect.id }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.
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)
return {
componentSetId: componentSet.id,
componentIds: components.map(c => c.id)
}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.
// Batch independent imports with Promise.all — these are independent IPC
// calls; awaiting them one after another doubles the round-trip latency.
const [comp, compSet] = await Promise.all([
figma.importComponentByKeyAsync("COMPONENT_KEY"),
figma.importComponentSetByKeyAsync("COMPONENT_SET_KEY"),
])
const instance = comp.createInstance()
instance.x = 40
instance.y = 40
figma.currentPage.appendChild(instance)
// Select a variant from the imported component set
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)
return {
componentId: comp.id,
componentSetId: compSet.id,
placedInstanceIds: [instance.id, variantInstance.id]
}Component Set with Variable Modes (Full Pattern)
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, modeId)
components.push(comp)
}
// 4. Combine into component set
const componentSet = figma.combineAsVariants(components, figma.currentPage)
componentSet.name = "Button"
return {
componentSetId: componentSet.id,
colorCollectionId: colors.id
}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
// 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
return { collId: coll.id, modeIds, varIds };Call 2: Create components using stored IDs, combine and layout
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 = async (id) => await figma.variables.getVariableByIdAsync(id);
const bindColor = async (varId) => figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', await getVar(varId)
);
const collection = await figma.variables.getVariableCollectionByIdAsync(collId);
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 = [await bindColor(varIds[`bg/${state}`])];
comp.setExplicitVariableModeForCollection(collection, 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.resize(cs.width + 200, cs.height + 200);
return { csId: cs.id, count: components.length };Read Existing Nodes and Return Data
const page = figma.currentPage
// findAllWithCriteria is hundreds of times faster than findAll for a pure
// type filter — the engine uses an internal type index instead of running
// a JS predicate on every node.
const nodes = page.findAllWithCriteria({ types: ['FRAME'] })
const data = nodes.map(n => ({
id: n.id,
name: n.name,
width: n.width,
height: n.height,
// `n.children` is safe here because the findAll predicate restricts to FRAME.
// Do NOT use `n.children?.length` defensively on an unfiltered node — the
// property access itself throws `no such property 'children'` on leaf types
// like TEXT/RECTANGLE, and optional chaining does not catch that.
childCount: n.children.length
}))
return { frames: data }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
- Slots: createSlot and SLOT Properties
- 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:
Follows the canonical text-edit recipe — load the font for every (family, style) you'll mutate (here Inter Regular; same rule for every other font) before any characters/fontName/fontSize write.
// Load required font BEFORE any text mutation
await figma.loadFontAsync({ family: "Inter", style: "Regular" });
// 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
Slots: createSlot and SLOT Properties
Slots are designated drop zones inside a component where designers can place arbitrary content in instances — more flexible than INSTANCE_SWAP (which only swaps component instances). They appear as SlotNode (type 'SLOT') in the Plugin API and as a SLOT-typed component property.
Option 1 — component.createSlot() (preferred)
Creates a SlotNode as a direct child of the component and automatically creates a linked SLOT component property. No manual wiring needed.
const card = figma.createComponent();
card.name = "Card";
card.layoutMode = "VERTICAL";
card.primaryAxisSizingMode = "AUTO";
card.counterAxisSizingMode = "FIXED";
card.resize(320, 100);
// Creates a SlotNode and auto-wires a SLOT component property
const contentSlot = card.createSlot();
contentSlot.name = "Content";
contentSlot.layoutMode = "VERTICAL"; // GRID is NOT allowed on slots
contentSlot.resize(320, 200);
// The auto-created property key is accessible via componentPropertyReferences
const slotPropKey = contentSlot.componentPropertyReferences["slotContentId"];
// e.g. "Content#7:1"Multiple slots are supported — each call to createSlot() produces a separate slot and property:
const contentSlot = card.createSlot();
contentSlot.name = "Content";
const footerSlot = card.createSlot();
footerSlot.name = "Footer";
// Component now has two SLOT properties automatically
return Object.keys(card.componentPropertyDefinitions);
// → ["Content#7:1", "Footer#7:2"]Option 2 — Manual binding via addComponentProperty
Link a regular frame to a SLOT property with componentPropertyReferences:
const slotPropKey = component.addComponentProperty("Content", "SLOT", "");
const slotFrame = figma.createFrame();
component.appendChild(slotFrame);
// slotFrame must not have GRID layoutMode, and must be a direct child (not nested inside another slot)
slotFrame.componentPropertyReferences = { slotContentId: slotPropKey };Populating slots in instances
In a component instance, slot nodes are accessible by findOne(). Build content and append it to the slot like any other node. In narrow cases the original node handle can be invalidated by the append, so if a post-append edit throws "Internal Figma Error: Parent not found", re-find the sublayer through the slot's children and edit through the fresh handle.
const instance = card.createInstance();
figma.currentPage.appendChild(instance);
const btn = figma.createFrame();
btn.layoutMode = "HORIZONTAL";
btn.cornerRadius = 8;
// Use the type-indexed criteria for the type filter, then narrow by name.
const contentSlot = instance
.findAllWithCriteria({ types: ["SLOT"] })
.find(n => n.name === "Content");
contentSlot.appendChild(btn);
// If a post-append edit throws "Parent not found", re-find via the slot:
// const appended = contentSlot.children[contentSlot.children.length - 1];
// appended.someProperty = ...;Slot restrictions
GRIDlayoutMode is not allowed on slot nodes- Widgets, Stickies, and ComponentNodes cannot be appended directly to a slot
- Frames nested inside another slot cannot themselves be bound to a slot property
instance.setProperties({ [slotPropKey]: ... })throws — slot content is set by appending children, not viasetPropertiesslotNode.resetSlot()(in an instance) reverts the slot to its default empty state
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
search_design_system (MCP tool) is an option for published components. For on-canvas components, don't loop pages inside one script — even for read-only discovery.
Prefer the two-step fan-out:
// Step 1 — one cheap use_figma call, no page switch. Returns the page IDs to fan out over.
return figma.root.children.map(p => ({ id: p.id, name: p.name }));Then in the next assistant turn, emit one `use_figma` per page in parallel (a single message with N tool-use blocks). Each script runs:
// Read-only discovery — skip invisible instance interiors for speed.
figma.skipInvisibleInstanceChildren = true;
// Step 2 — one call per page, currentPage set exactly once.
// The agent MUST issue N of these in parallel in one message — do not loop pages inside the script.
const page = await figma.getNodeByIdAsync(PAGE_ID); // PAGE_ID supplied by caller
await figma.setCurrentPageAsync(page);
// Indexed type lookup — much faster than findAll with a side-effect predicate.
const matches = page.findAllWithCriteria({ types: ['COMPONENT', 'COMPONENT_SET'] });
return matches.map(n => ({ pageName: page.name, name: n.name, type: n.type, id: n.id }));See gotchas.md → Set current page once per `use_figma` call for the full rule.
Inspect an existing component set's variant naming pattern
const cs = await figma.getNodeByIdAsync('COMPONENT_SET_ID');
const variantNames = cs.children.map(c => c.name);
const propDefs = cs.componentPropertyDefinitions;
return { variantNames, propDefs };Find existing components in the file
search_design_system is an option for published components. For on-canvas components, use the two-step fan-out from the section above — don't loop pages inside one script.
Step 1 — one cheap use_figma call returns page IDs:
return figma.root.children.map(p => ({ id: p.id, name: p.name }));Step 2 — the agent MUST emit one use_figma per page in parallel (a single message with N tool-use blocks). Each script:
// Read-only discovery — skip invisible instance interiors for speed.
figma.skipInvisibleInstanceChildren = true;
const page = await figma.getNodeByIdAsync(PAGE_ID);
await figma.setCurrentPageAsync(page);
// Indexed type lookup — much faster than findAll with a side-effect predicate.
const components = page.findAllWithCriteria({ types: ['COMPONENT'] });
return components.map(n => ({ name: n.name, id: n.id, page: page.name, w: n.width, h: n.height }));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.
// Batch independent imports with Promise.all — sequential awaits multiply
// IPC latency by the number of imports for no benefit.
const [comp, set] = await Promise.all([
figma.importComponentByKeyAsync("COMPONENT_KEY"),
figma.importComponentSetByKeyAsync("COMPONENT_SET_KEY"),
]);
const instance = comp.createInstance();
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 } }
return propDefs;Also check nested instances — a parent component may not expose text properties directly, but its nested child instances might:
const nestedInstances = instance.findAllWithCriteria({ types: ["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:
// Use the type-indexed criteria for the type filter, then narrow by name.
const nestedHeading = instance
.findAllWithCriteria({ types: ["INSTANCE"] })
.find(n => 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.findAllWithCriteria({ types: ["TEXT"] });
// Dedupe fonts and load them in parallel before mutating text. Awaiting
// loadFontAsync per node in the loop serializes one IPC round-trip per
// text node and reloads the same font repeatedly.
const uniqueFonts = [...new Map(
textNodes.map(t => [JSON.stringify(t.fontName), t.fontName])
).values()];
await Promise.all(uniqueFonts.map(f => figma.loadFontAsync(f)));
for (const t of textNodes) {
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
// For local components, use getLocalComponentMetadata:
const result = await getLocalComponentMetadata('COMPONENT_OR_SET_ID');
return result;
// For published components, use getPublishedComponentMetadata:
// const result = await getPublishedComponentMetadata('COMPONENT_KEY');
// return result;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
- Importing Library Effect Styles
- 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:
const results = await listEffectStyles();
return results;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:
const style = createDropShadowStyle(
"Elevation/200",
{ r: 0, g: 0, b: 0, a: 0.15 },
{ x: 0, y: 4 },
12,
0
);
return { id: style.id, name: style.name };Importing Library Effect Styles
For effect styles from team libraries, use importStyleByKeyAsync:
// Import a library effect style by key
const shadowStyle = await figma.importStyleByKeyAsync("EFFECT_STYLE_KEY");
// Apply to a node
node.effectStyleId = shadowStyle.id;search_design_system with includeStyles: true returns style keys you can import this way. Prefer importing library styles over creating new ones.
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:
const applied = applyEffectStyleToMatchingNodes('STYLE_ID', 'Card');
return { applied };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. The sync setter figma.currentPage = page does NOT work and will throw — always use the async method.
const targetPage = figma.root.children.find(p => p.name === "My Page");
await figma.setCurrentPageAsync(targetPage);
// targetPage.children is now populatedReturning Results
Scripts are automatically wrapped in an async IIFE with error handling. Just write plain JS and use return to send data back to the agent:
// Return an object — auto-serialized to JSON
return { nodeId: frame.id, count: 5 }
// Return a string
return "Created 3 components"Errors are automatically captured — no try/catch needed. figma.notify() does not exist. Return all information via the return value.
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
Canonical recipe: load font → await → mutate → return affected IDs. This pattern is the same for every font — Inter happens to be preloaded so the missing-loadFontAsync bug usually only surfaces with other families. See gotchas.md → Canonical text-edit recipe.
// Load font BEFORE any text mutation — required for every font, not just Inter
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
Prefer `figma.createAutoLayout()` — it returns a frame with layoutMode already set and both axes hugging content, so children can immediately use layoutSizingHorizontal/Vertical = "FILL".
const frame = figma.createAutoLayout(); // HORIZONTAL by default
const column = figma.createAutoLayout("VERTICAL");
// Customize from there as usual:
frame.itemSpacing = 16;
frame.paddingTop = 24;
frame.paddingBottom = 24;
frame.paddingLeft = 24;
frame.paddingRight = 24;If you need a non-auto-layout frame, use figma.createFrame() and set the properties manually:
const frame = figma.createFrame();
frame.layoutMode = "VERTICAL"; // or "HORIZONTAL"
frame.resize(360, 1); // Width fixed, height auto
frame.primaryAxisSizingMode = "AUTO"; // Hug main axis
frame.counterAxisSizingMode = "FIXED"; // Fixed cross axisCRITICAL ORDERING: Always call resize() BEFORE setting sizing modes. The resize() method silently resets both sizing modes to FIXED, so calling it after setting primaryAxisSizingMode = "AUTO" will override your HUG settings and lock the frame to the exact pixel dimensions you passed (even throwaway values like 1). This causes the common "1px dimension" bug.
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.resize(800, 600); // `resize` and `resizeWithoutConstraints` are equivalent on sections
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().
// Batch independent imports with Promise.all — sequential awaits multiply
// IPC latency by the number of imports for no benefit.
const [comp, set] = await Promise.all([
figma.importComponentByKeyAsync(componentKey),
figma.importComponentSetByKeyAsync(componentSetKey),
])
const instance = comp.createInstance()
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
// Load font BEFORE setting style.fontName — required for every font, not just Inter
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
// If you already have the node's ID, NEVER scan — use the indexed lookup.
const known = await figma.getNodeByIdAsync("123:456");
// Find by name on current page (no ID available)
const node = figma.currentPage.findOne(n => n.name === "My Frame");
// Find all by type — use findAllWithCriteria, hundreds of times faster than
// findAll(n => n.type === '…') because the engine uses an internal type index.
const allTexts = figma.currentPage.findAllWithCriteria({ types: ["TEXT"] });
// Find all by name pattern (predicate is fine — criteria doesn't index name)
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.
>
Every example here assumes the canonical text-edit recipe: load font →await→ mutate → return affected IDs. Examples useInterbecause it's available everywhere, but the rule applies identically to any font family/style.
>
For design system context (when to create text styles, how they relate to tokens, use_figma limitations), see wwds-text-styles.Contents
- Listing Text Styles
- Creating a Text Style
- Discovering Available Font Styles
- Creating a Type Ramp (Multi-Step)
- Importing Library Text Styles
- 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:
const results = await listTextStyles();
return results;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;
}Discovering Available Font Styles
Font style names vary per provider and per file. Use figma.listAvailableFontsAsync() to discover exact style strings — never guess or probe with try/catch:
/**
* Discovers available font styles for a given family using listAvailableFontsAsync.
*
* @param {string} family - Font family name, e.g. "Inter"
* @returns {Promise<string[]>} - All available style names for the family
*/
async function getAvailableFontStyles(family) {
const allFonts = await figma.listAvailableFontsAsync();
return allFonts
.filter(f => f.fontName.family === family)
.map(f => f.fontName.style);
}Creating a Type Ramp (Multi-Step)
Handles font loading, deduplication, and idempotency. Each entry: [name, fontFamily, fontStyle, fontSize_px, lineHeight, cssVar].
/**
* 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:
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);
return result;Importing Library Text Styles
For text styles from team libraries, use importStyleByKeyAsync:
// Import a library text style by key
const headingStyle = await figma.importStyleByKeyAsync("TEXT_STYLE_KEY");
// Apply to a text node
await textNode.setTextStyleIdAsync(headingStyle.id);search_design_system with includeStyles: true returns style keys you can import this way. Prefer importing library styles over creating new ones.
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'] });
const matching = textNodes.filter(n => n.name.includes(nodeNamePattern));
// Batch the style applications with Promise.all — each setTextStyleIdAsync
// call is independent, so awaiting them serially in a for-loop multiplies
// the IPC latency by the number of matches.
await Promise.all(matching.map(n => n.setTextStyleIdAsync(styleId)));
return matching.length;
}Full runnable script:
const applied = await applyTextStyleToMatchingNodes('STYLE_ID', 'Heading');
return { applied };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 - 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
Error Recovery After Failed use_figma
`use_figma` is atomic — failed scripts do not execute. If a script errors, no changes are made to the file. The file remains in exactly the same state as before the call. There are no partial nodes, no orphaned elements, and retrying after a fix is safe.
Recovery steps when `use_figma` returns an error: 1. STOP — do NOT immediately fix the code and retry. Read the error message carefully first. 2. Understand the error. Most errors are caused by wrong API usage, missing font loads, invalid property values, or referencing nodes that don't exist. 3. If the error is unclear, call get_metadata or get_screenshot to understand the current file state and confirm nothing has changed. 4. Fix the script based on the error message. 5. Retry the corrected script.
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. Read the error message carefully
b. get_metadata / get_screenshot → If the error is unclear, inspect file state
c. Fix the script based on the error
d. Retry the corrected script (safe — failed scripts don't modify the file)Variable & 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)
- Importing Library Variables
- 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, 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 set scopes explicitly — ALL_SCOPES is the default but almost never what you want. For a comprehensive scope-to-use-case mapping table, see token-creation.md § Variable Scopes — Complete Reference Table.
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})`Importing Library Variables
For variables from team libraries (not the current file), use importVariableByKeyAsync:
// Import a single variable by its key
const colorVar = await figma.variables.importVariableByKeyAsync("VARIABLE_KEY");
// Now use it like any local variable
const paint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', colorVar
);
node.fills = [paint];To discover available library variable collections and their variables:
// List all available library variable collections
const libCollections = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();
// Each has: name, key, libraryName
// Get variables in a specific library collection
const libVars = await figma.teamLibrary.getVariablesInLibraryCollectionAsync(libCollections[0].key);
// Each has: name, key, resolvedType
// Import the ones you need:
const imported = await figma.variables.importVariableByKeyAsync(libVars[0].key);When to import vs. use local: If variable.remote === true, it's from a library — you can reference it directly if already imported, or import by key. If remote === false, it's local to the file — use getVariableByIdAsync directly.
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
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 => ({ name: m.name, id: m.modeId }))
}));
return results;Inspect scope patterns used in existing variables
const collections = await figma.variables.getLocalVariableCollectionsAsync();
// Batch every getVariableByIdAsync into a single Promise.all — sequential
// awaits inside the loop would serialize one IPC round-trip per variable.
const allIds = collections.flatMap(c => c.variableIds);
const allVars = await Promise.all(
allIds.map(id => figma.variables.getVariableByIdAsync(id))
);
const scopeGroups = {};
for (const v of allVars) {
if (!v) continue;
const key = JSON.stringify(v.scopes);
if (!scopeGroups[key]) scopeGroups[key] = [];
scopeGroups[key].push(v.name);
}
return scopeGroups;Build a name→variable lookup for reuse
const varByName = {};
for (const v of await figma.variables.getLocalVariablesAsync()) {
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();
// flatMap every variable ID across all collections into one Promise.all,
// then look up per-collection from a Map. Avoids nested Promise.all and
// still issues every lookup in parallel.
const allIds = collections.flatMap(c => c.variableIds);
const fetched = await Promise.all(
allIds.map(id => figma.variables.getVariableByIdAsync(id))
);
const byId = new Map(allIds.map((id, i) => [id, fetched[i]]));
return collections.map(collection => ({
name: collection.name,
id: collection.id,
modes: collection.modes.map(m => [m.name, m.modeId]),
variables: collection.variableIds
.map(id => byId.get(id))
.filter(v => v)
.map(v => [v.name, v.id, v.codeSyntax, v.scopes])
}));
}Full runnable script:
const results = await listVariableCollectionsAndVariables();
return results;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;