
Figma Use
- 5.6k installs
- 1.8k repo stars
- Updated July 30, 2026
- figma/mcp-server-guide
figma-use is a mandatory agent skill prerequisite for use_figma MCP calls that execute JavaScript against the Figma Plugin API in design files.
About
The figma-use skill is the mandatory prerequisite before every use_figma MCP tool call when agents write to or programmatically inspect Figma design files. It documents Plugin API execution rules: top-level await and return for output, never figma.notify or console.log, 0-1 color channels, clone-and-reassign fills, async page switching via setCurrentPageAsync, and the canonical font-load-before-text-edit recipe. Agents work incrementally with at most ten logical operations per call, return all created or mutated node IDs, fan multi-page edits into parallel use_figma invocations, and stop on atomic script errors before retrying fixed code. Efficient APIs cover node.query selectors, node.set batch updates, createAutoLayout containers, placeholder shimmer feedback, and inline node.screenshot validation. Pair with figma-generate-design for full screens and figma-generate-library for component libraries. References span gotchas, variables, components, text styles, and design-system conventions.
- Mandatory prerequisite before every use_figma MCP Plugin API script execution.
- Incremental inspect-build-validate workflow with returned node IDs each step.
- Auto-layout, font-load, page-switch, and sizing gotchas with fix patterns.
- Efficient node.query, node.set, createAutoLayout, and screenshot APIs.
- Pairs with figma-generate-design and figma-generate-library for larger builds.
Figma Use by the numbers
- 5,611 all-time installs (skills.sh)
- +184 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #66 of 1,888 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
figma-use capabilities & compatibility
- Capabilities
- plugin api javascript execution via use_figma mc · incremental node create, mutate, and id return c · auto layout, variable binding, and text edit got · multi page parallel fan out and atomic error rec
- Works with
- figma
- Use cases
- ui design · web design · documentation · orchestration
What figma-use says it does
you MUST invoke this skill BEFORE every `use_figma` tool call.
NEVER call `use_figma` directly without loading this skill first.
Work incrementally in small steps.
`use_figma` is atomic — failed scripts do not execute.
npx skills add https://github.com/figma/mcp-server-guide --skill figma-useAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5.6k |
|---|---|
| repo stars | ★ 1.8k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 30, 2026 |
| Repository | figma/mcp-server-guide ↗ |
How do agents avoid hard-to-debug use_figma failures when creating nodes, editing text, switching pages, or binding design tokens in Figma?
Mandatory prerequisite before every use_figma MCP call to create, edit, or inspect Figma files via Plugin API JavaScript.
Who is it for?
Agents automating Figma design files via MCP who need create, edit, variable, component, and layout operations.
Skip if: Skip when only exporting static screenshots without Plugin API writes or when another figma-use sibling skill owns FigJam or Slides context alone.
When should I use this skill?
User performs write actions or unique reads needing JavaScript in Figma: create or edit nodes, variables, components, auto-layout, fills, or programmatic file inspection.
What you get
Reliable incremental Figma canvas changes with validated scripts, returned node IDs, and documented recovery when Plugin API rules are violated.
- Edited Figma document nodes
- Auto-layout frames and components
- Exported SVG or image assets
By the numbers
- Documents 10+ Plugin API topic areas including node creation, variables, SVG, and traversal
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 include `figma-use` in the comma-separated `skillNames` parameter when calling `use_figma`. If this skill was loaded via an MCP resource, you MUST prefix the name with `resource:` (e.g. `resource:figma-use`). 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. 12b. *`layoutSizing and AxisSizingMode` are different enums — don't cross them. `layoutSizingHorizontal`/`layoutSizingVertical` (set on a child) take `'FIXED'|'HUG'|'FILL'`; `primaryAxisSizingMode`/`counterAxisSizingMode` (set on the frame itself) take `'FIXED'|'AUTO'`. So `layoutSizingVertical = 'AUTO'` is invalid (use `'HUG'`), and `counterAxisSizingMode = 'FILL'` throws `Expected 'FIXED' | 'AUTO', received 'FILL'` (use `'FIXED'`/`'AUTO'`). Two more errors from the same setter — `Error: in set_layoutSizingHorizontal: node must be an auto-layout frame or a child of an auto-layout frame` and `Error: in set_layoutSizingHorizontal: FILL can only be set on children of auto-layout frames` — mean the node isn't in an auto-layout context yet; recommendation: make the parent auto-layout (`figma.createAutoLayout()`) and `appendChild` the node before setting (see Rule 12). See [Gotchas](references/gotchas.md#layoutsizing-vs-axissizingmode-two-different-sizing-enums). 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](references/gotchas.md). 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](#6-error-recovery--self-correction). 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](references/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. Slides override: in Slides files, slides are isolated subtrees — the relevant limit is complexity per slide, not total nodes across slides. Building 3–5 new slides in one call is safe, and so is applying the same edit (e.g. adding a footer, recoloring a heading) across every slide in the deck in a single call. See figma-use-slides for the deck-building workflow.
- 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 |
Error: in set_layoutSizingHorizontal: node must be an auto-layout frame or a child of an auto-layout frame / Error: in set_layoutSizingHorizontal: 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) - [ ] Wrapping TEXT blocks set
textAutoResize = 'HEIGHT'and an explicit width ('FIXED'+resize()) — NOTFILLalone, which the defaultWIDTH_AND_HEIGHTmode ignores, collapsing the node to a near-zero-width thread. Verifynode.width > 0 - [ ] 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 (Frames, Text, Rectangles, Ellipses, Lines, Vectors, SVG Import)
- Fills and Strokes (Solid, Linear Gradient, Radial Gradient, Multiple Fills)
- 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;Vectors (Custom Paths)
Use figma.createVector() for any shape that can't be built from primitives — organic curves, flowing contours, custom silhouettes. The vectorPaths property accepts SVG path data.
const vector = figma.createVector();
vector.vectorPaths = [{
windingRule: "NONZERO",
data: "M 0 60 C 30 10 70 10 100 50 C 130 90 170 20 200 40 L 200 100 L 0 100 Z"
}];
vector.fills = [{ type: 'SOLID', color: { r: 0.15, g: 0.3, b: 0.55 } }];
vector.strokes = [{ type: 'SOLID', color: { r: 0.1, g: 0.2, b: 0.4 } }];
vector.strokeWeight = 2;SVG path commands: M (move to), L (line to), C x1 y1 x2 y2 x y (cubic bezier), Q x1 y1 x y (quadratic bezier), A rx ry rot large sweep x y (arc), Z (close). Uppercase = absolute, lowercase = relative.
Vectors support all the same properties as other shapes — fills, strokes, effects, brushes, dynamic strokes, variable-width strokes, and texture effects.
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]] // left-to-right (default)
}];
// Top-to-bottom:
// gradientTransform: [[0, 1, 0], [-1, 0, 1]]Radial Gradient
node.fills = [{
type: "GRADIENT_RADIAL",
gradientStops: [
{ color: { r: 0.95, g: 0.7, b: 0.3, a: 1 }, position: 0 }, // bright center
{ color: { r: 0.82, g: 0.38, b: 0.14, a: 1 }, position: 1 } // deeper edge
],
// 2x3 affine matrix [[scaleX, shearX, tx], [shearY, scaleY, ty]]
// Scale <1 = gradient LARGER than shape (freeform color — use for illustration)
// Scale >1 = gradient SMALLER than shape (obvious oval — avoid for illustration)
gradientTransform: [[0.71, 0, 0.19], [0, 0.57, 0.21]] // large, offset
}];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)Working with design systems: Creating Components
When creating Figma components, you need to start by understanding the source and its intent.
If the user is asking you to create a component based on a design or specification, you need to understand the property model before you build anything. What variants are needed? What text, boolean, or instance swap properties exist? Getting the structure right upfront matters because restructuring a component after instances exist is destructive.
If you are given a code component as reference (React props, tokens, etc.), your goal is to reflect the property surface as closely as makes sense in Figma's model. Not all code properties translate directly — hover and focus states are not props in web code, but they are variants in Figma. Understand those gaps and make deliberate decisions about how to represent them.
Variants are the most important thing to get right. Each combination of variant values creates a node on the canvas. Redundant combinations still exist as explicit nodes — there is no way to conditionally exclude them. Define only the axes you actually need.
Non-variant properties (text, boolean, instance swap) should be added after the variant structure is established. These are defined at the component/component set level and referenced by descendant nodes via componentPropertyReferences. Always connect them — a property that isn't wired to a descendant is invisible to users of the component.
If the user asks you to make architectural decisions, lean toward fewer variants and more boolean/text properties where possible. Variants multiply combinatorially; the other property types do not. An optional slot property in code might be a combination of instance swap and boolean visibility.
When naming properties, casing is less important since translation layers like Code Connect can do the mapping to represent the code form. Feel free to take a sentence or capitalized case approach for better readability in Figma.
Keep in mind that components often need to be published and connected to Code Connect for the full design-to-code workflow to work. Creating the component is only one part of the system.
Working with design systems: Using Components
When using Figma components, you need to start by understanding the state of the source and the state of Figma.
For the source, you need to know what component is being referenced. This could come from a component key, a node ID, a name, or a Code Connect mapping. If you have a component key from a design system library, prefer importComponentByKeyAsync over finding by name, since names are not unique. If you only have a name, search the page or use search_design_system to find the right match.
For Figma, you need to know whether the component is local or in a library. Local components can be accessed directly by node ID. Published library components must be imported first — importComponentByKeyAsync or importComponentSetByKeyAsync — before an instance can be created.
Before setting properties on an instance, read componentPropertyDefinitions from the main component first. Property names are not simple strings — TEXT, BOOLEAN, and INSTANCE_SWAP properties have a #uid suffix (e.g. "Label#1234"). Only VARIANT properties are plain names (e.g. "Size"). Using the wrong key in setProperties will silently do nothing.
A component might have multiple text properties, which are not possible to derive from text node layer names. Look to the properties to help you understand what values to set, rather than thinking of setting text node characters directly.
When you need to set a nested instance swap (e.g. an icon property), you need the component key of the swap target, not just its name. Import the target component and pass its node ID.
Be aware that instances inside other instances are nested and changes made to a nested instance may be treated as overrides. If the intent is to change the default appearance, you need to modify the main component, not the instance.
When selecting which variant to use, read the componentProperties on the instance to see the current state, and componentPropertyDefinitions on the main component to see all available options.
Components
Components overlap a lot with the idea of components in a codebase, but with some gaps and other Figma-specific use cases. Components in Figma can be reusable entities that do not have a comparable library pattern, or they can be published and distributed in a library that is aligned to a code forms.
Properties can vary from code in different ways, but alignment to code can still happen without a direct relationship. For example, an interactive pattern in code (like a button) can have many states. A lot of these states (active, focused etc) would be expressed in Figma as variants, which is a concept more closely aligned to properties in a code library. In the case of web this is confusing since hover is not a prop, it is a pseudo selector. At the same time, a color variant might be perfectly aligned between design and code (a property in both places). These discrepancies are accounted for in translation with Figma's Code Connect (deterministic context mapping), but in the case of these tools, must be understood to be properly used.
Figma has four property types, which can be inspected in the component definition's componentPropertyDefinitions. To fully understand the component, its descendants must be traversed. Property types include:
- Variant
- This is reflected as permutations of the component in a Component Set on the canvas. Each variant is explicitly visualized, including an redundant permutations ("Small + Primary + Disabled" may look the same as "Small Secondary Sisabled"). These permutations create different variants implicitly in Figma and it is handled through layer naming (
Variant=Primary,Size=Small,State=Disabled). - Text/String
- Text properties are stored on the component parent, but can be mapped to Text node descendants.
node.componentPropertyReferences.characterson a descendant text node are how you determine where the text property is referenced (can be multiple, though unlikely).- Boolean
- Boolean properties are stored on the component parent, but can be mapped to any node descendant that can have its visibility toggled.
node.componentPropertyReferences.visibleon a descendant node are how you determine where the boolean property is referenced.- Instance Swap
- Instance swap properties are stored on the component parent, but can be mapped to Instance node descendants.
node.componentPropertyReferences.mainComponenton a descendant instance node are how you determine where the instance property is referenced. A classic example of this is an icon property.
Descriptions
Components, component sets, and instances all inherit PublishableMixin, which includes a writable description string. Setting a description is important for any component intended to be used by others — it appears in Figma's dev mode and component panel, and is surfaced when reading component metadata.
Descriptions should explain the component's intent and any non-obvious usage constraints. They are not a substitute for Code Connect annotations, but they are always visible without any tooling setup.
component.description =
"Primary action button. Use for the single most important action on a page.";Variant components (children of a component set) also have a description field, but in practice the component set description is what users see. Set it on the component set, not on individual variant nodes.
To read descriptions when auditing:
// Get all component sets and their descriptions
figma.root
.findAllWithCriteria({ types: ["COMPONENT_SET"] })
.map((n) => ({ name: n.name, description: n.description }));Usage guidelines
- Creating components: What you must consider when creating new components.
- Using components: What you must consider when trying to use the right components.
Code patterns
For runnable code examples (creating, importing, discovering, inspecting components), see component-patterns.md.
Working with design systems: Effect Styles
Effect styles in Figma are named, reusable definitions of one or more visual effects — drop shadows, inner shadows, and blurs. They are the closest equivalent to a shadow or elevation token in a design system.
Effect styles are distinct from variables. There is no single variable type that represents a shadow. However, individual numeric and color properties within an effect _can_ be bound to variables, allowing shadow values to participate in a token system.
Model
An EffectStyle has one core writable property beyond the base style fields:
| Property | Type | Notes |
|---|---|---|
name | string | Slash-delimited for grouping (e.g. "Elevation/200") |
effects | ReadonlyArray<Effect> | Read-only array — clone, modify, reassign |
description | string | Inherited from BaseStyleMixin |
Effect types
An Effect is a discriminated union. The most common types:
type | Key properties |
|---|---|
DROP_SHADOW | color: RGBA, offset: Vector, radius: number, spread: number, visible: boolean, blendMode |
INNER_SHADOW | Same as DROP_SHADOW |
LAYER_BLUR | radius: number, visible: boolean |
BACKGROUND_BLUR | radius: number, visible: boolean |
All colors are in 0–1 range (RGBA), not 0–255.
Variable bindings on effects
Effect properties that can be bound to variables (via setBoundVariableForEffect(effect, field, variable) on a node, or inline when constructing):
color, radius, spread, offsetX, offsetY
Note: setBoundVariableForEffect returns a new effect object — you must capture it and reassign the effects array.
Applying an effect style to a node
Assign the style's id to the node's effectStyleId. The node's effects property will then reflect the style's values.
Common gotchas
- `effects` is read-only: You cannot mutate the array in place. Clone it, modify the clone, then reassign:
style.effects = [...style.effects, newEffect]. - Effects stack in order: The order of effects in the array matters visually. Drop shadows render bottom-to-top.
- Colors are RGBA 0–1:
{ r: 0, g: 0, b: 0, a: 0.15 }— not hex, not 0–255. - `getLocalEffectStyles()` is deprecated: Always use
getLocalEffectStylesAsync(). - Styles are not automatically applied: Creating an
EffectStylehas no effect on any node until you assign its ID to a node.
Code patterns
For runnable code examples (listing, creating, applying effect styles), see effect-style-patterns.md.
Related skills
Forks & variants (2)
Figma Use has 2 known copies in the catalog totaling 201 installs. They canonicalize to this original listing.
- thedesignproject - 148 installs
- figma - 53 installs
How it compares
Use figma-use when agents must call the Figma Plugin API directly; use a static design review skill when no live Figma file manipulation is required.
FAQ
Must I load figma-use before every use_figma call?
Yes. The skill is a mandatory prerequisite; skipping it causes common, hard-to-debug failures per the description.
How should agents output data from use_figma scripts?
Use return with structured JSON; console.log is not returned and figma.notify throws not implemented.
What happens when a use_figma script errors?
The script is atomic and makes no file changes; read the error, fix the script, then retry without immediate blind retries.
Is Figma Use safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.