use_figma β Figma Plugin API Skill
Execute JavaScript in Figma files via the Plugin API. Always pass skillNames: "figma-use" when calling use_figma (logging parameter, doesn't affect execution).
If the task involves building or updating a full page, screen, or multi-section layout in Figma from code, also load figma-generate-design. It provides the workflow for discovering design system components via search_design_system, importing them, and assembling screens incrementally. Both skills work together: this one for the API rules, that one for the screen-building workflow.
If the task involves creating or building a component in Figma (even a single component), also load figma-generate-library. It owns the component-creation workflow β variable foundations first, variant sets, then design token bindings β that figma-use alone doesn't cover. Build the token/variable foundation before the component, then bind the component's fills/cornerRadius/spacing to those variables rather than inlining literals where a token exists.
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.
$fig β the plan-based builder API (ALL NODE CREATION MUST USE THIS)
$fig is a global that is responsible for all node creation. It auto-flushes at script end (no $fig.done() needed), handles font preloading, batches mutations, and orders property assignment correctly. Use it for all node creation and mutation operations. Never use figma.createFrame(), figma.createText() or any figma.create* methods. They do not exist in this environment.
Copy these patterns
Build an auto-layout frame with children β single call:
$fig.autoLayout(
// fixed width; omit height to hug vertically
{ name: 'Todo List', layoutMode: 'VERTICAL', width: 480 },
['item 1', 'item 2', 'item 3'].map((item) =>
$fig.autoLayout(
// Set `layoutSizingHorizontal` to FILL since auto-layout is hug x hug by default
{ name: 'Todo Item', layoutSizingHorizontal: 'FILL' },
[$fig.text({ characters: item, fontName: { family: 'Inter', style: 'Bold' } })],
),
),
).screenshot() // Screenshot new node trees to verify. Prefer `.screenshot()` over `get_screenshot` call after `use_figma`.N parallel items for repeated small UI elements like swatches, list items, etc.:
const ITEMS = [
{ name: 'A', bg: hex('#ffffff'), accent: hex('#0969da') },
{ name: 'B', bg: hex('#fff8f1'), accent: hex('#bf5af2') },
// ...add more here
]
ITEMS.forEach((v, i) => $fig.autoLayout({ name: v.name, x: i * 410, width: 390, fills: [{ type:'SOLID', color: v.bg }] }, [
$fig.text({ characters: v.name, fills: [{ type:'SOLID', color: v.accent }] }),
]))Update existing nodes β by query or by id:
$fig.query('FRAME[name=Header] TEXT[name=Title]').set({ characters: 'New Title', fontSize: 24 })
$fig.get('1:42').set({ opacity: 0.8, cornerRadius: 12 })Add a node inside an existing node
$fig.get('1:42').append($fig.autoLayout({ name: 'New Frame' }))Create a component with a few different variants
const SIZES = ['Small', 'Medium', 'Large']
$fig.variants({ name: 'Button' }, SIZES.map((size) => $fig.component({ name: `Size=${size}`, layoutMode: 'HORIZONTAL', /** other props */ })))β οΈ
$fig.variantsdoes not position the variants β they stack at (0,0) and the set renders as one collapsed, overlapping element. You must grid the variants and resize the set afterward. Seefig-builder.mdfor the required follow-up recipe.
Building a component β even a single one? Binding tokenized values is part of finishing the job, not a preference. A component is not complete while any value that has a corresponding design token β one that already exists in the file, or that you created from the source β is still a hardcoded literal. When a token exists for a
fillscolor /cornerRadius/ padding /itemSpacing, bind it: build a$fig.varCollection(primitive tier + semantic tier aliased to it) and pass the variable handle straight into the property. Anti-pattern to avoid: do NOT copy resolved token values into local JS constants (e.g.const VARIANTS = [{ bg: '#2c2c2c' }]) and paint withhex(...)β that silently bypasses variables even though the source defines tokens. This applies to a single component as much as a full design system. Only bind values that actually have a token β values with genuinely no token (one-off geometry, icon pixel sizes, static 1px dividers) correctly stay literal; don't invent tokens to bind. If you create the variables in oneuse_figmacall and build the component in a later one, rehydrate the handles first ($fig.getVar(id)orfigma.variables.getVariableByIdAsyncusing the IDs you returned) β a handle from a previous call isn't in scope. Worked recipe: fig-builder.md β Building a component with bound variables.
Create an instance of a component
// First arg can be a node ID ('1:2') OR a library asset key
// from `search_design_system` results (the `componentKey` field).
$fig.instance('1:2', { name: 'Cancel Btn', props: { label: 'Cancel'}})Consume styles/variables
$fig.autoLayout({ name: 'Card', itemSpacing: spacingVar, fills: fillStyle })
// Looked up by asset key from `search_design_system` (the `key` field)
$fig.autoLayout({ fills: $fig.getStyle(BG_STYLE_KEY) })
$fig.rectangle({ fills: [{ type: 'SOLID', color: $fig.getVar(BRAND_VAR_KEY) }] })Use design-system assets by key (from search_design_system)
search_design_system returns componentKey for components and component sets, and key for styles and variables. Pass these straight into the unified $fig lookup β the plan queues the library import automatically, so you don't need a separate await figma.importComponentByKeyAsync(...) / importStyleByKeyAsync(...) / importVariableByKeyAsync(...) step.
// One call site, many input shapes β node IDs, real variable/style ids,
// AND 40-char asset keys (e.g. '49c8754d4b898e176148650df612a47998a8c4a1')
const btn = $fig.get(BUTTON_KEY) // component / component set
const instance = $fig.instance(BUTTON_SET_KEY, { // create an instance from a set
props: { Size: 'md', Variant: 'primary' },
})
const heading = $fig.getStyle(HEADING_TEXT_STYLE_KEY) // paint / text / effect / grid style
const brand = $fig.getVar(BRAND_COLOR_VAR_KEY) // variable
$fig.text({ characters: 'Hello', textStyle: heading })
$fig.rectangle({ fills: [{ type: 'SOLID', color: brand }] })Discover a set's variant props from its key β search_design_system returns a set's componentKey, not its variant properties. This is two use_figma calls: call 1 returns the set's property definitions and its variants so their props come back to you in the tool result; then, knowing the valid props, call 2 instantiates the variant you want.
const setHandle = $fig.get(BUTTON_SET_KEY)
await $fig.done()
const set = setHandle.node
if (!set || set.type !== 'COMPONENT_SET') {
throw new Error(`Key ${BUTTON_SET_KEY} is not a COMPONENT_SET`)
}
return {
componentPropertyDefinitions: set.componentPropertyDefinitions,
variants: set.children
.filter((c) => c.type === 'COMPONENT')
.map((c) => ({ name: c.name, variantProperties: c.variantProperties })),
}// Call 2 β instantiate with props you picked from call 1's output
$fig.instance(BUTTON_SET_KEY, { props: { Size: 'Large', Kind: 'Secondary' } })Bulk component swap β $fig.query().set():
const chevron = $fig.get('CHEVRON_KEY')
$fig.query('INSTANCE[name=arrow_drop_down]').set({ mainComponent: chevron }).query() and .values() for search and projecting child values
const menuArrows = $fig
.query('PAGE[name=Menu] INSTANCE[mainComponent.name*=arrow], PAGE[name=Menu] INSTANCE[mainComponent.name*=chevron]')
.values(['id', 'name', 'mainComponent.name', 'mainComponent.id']);
// Use quotes for multi-word selector values
const expandComponents = $fig
.query('COMPONENT[name*=expand], COMPONENT[name*=chevron_down], COMPONENT[name*=chevron_up], COMPONENT[name*="multi word name"]')
.values(['id', 'name']);Perform operations on another page:
// Switch to a specific page (loads its content)
const targetPage = figma.root.children.find((p) => p.name === "My Page");
await figma.setCurrentPageAsync(targetPage);
$fig.query('INSTANCE[name=arrow_drop_down]').set({ ... })Gradient via helper β no manual transform matrix needed:
$fig.gradient(node, 'LINEAR', [
{ position: 0, color: { r: 0, g: 0, b: 0 } },
{ position: 1, color: { r: 1, g: 1, b: 1 } },
])Hex helper at script top:
const hex = h => { const n = parseInt(h.replace('#',''), 16); return { r:((n>>16)&255)/255, g:((n>>8)&255)/255, b:(n&255)/255 } }
// then: color: hex('#2563eb')Chaining on plan nodes β alternative to children array:
const card = $fig.autoLayout({ name: 'Card', layoutMode: 'VERTICAL' })
card.text({ characters: 'Title', fontSize: 20 })
card.text({ characters: 'Description', fontSize: 14 })$fig create + mutate API (full surface)
- Create:
$fig.autoLayout / .frame / .text / .rectangle / .ellipse / .polygon / .star / .line / .vector / .section / .component / .pageβ all(opts?, children?). Plan nodes are chainable. - Create from SVG β the preferred ICON path:
$fig.svg(svgStr, opts?)builds a vector node tree from an SVG string. Prefer real vector icons: import the icon's SVG source (inline<svg>, the.svgasset, or the source icon-library glyph β e.g. lucide/heroicons) via$fig.svg(...)rather than approximating an icon with a typed emoji/Unicode glyph (β β π β° βΎ) or a plain rectangle. A simple glyph or shape is a fine fallback when the real SVG genuinely can't be obtained β just reach for the SVG first. (Don't reconstruct an icon from rotated line/rect primitives, though β that renders broken.) Full recipe (viewBox+width/height sizing,currentColor, INSTANCE_SWAP for design-system icons): figma-generate-design β Icons. - Create an instance of a component:
$fig.instance(compRef, opts?)βcompRefis a component plan node, a node ID string, OR a library asset key (componentKeyfromsearch_design_system); the import is queued in the plan automatically. - Grouping/boolean:
$fig.group / .union / .subtract / .intersect / .exclude / .variantsβ all(opts?, children?). - Read:
$fig.get(id)wraps an existingSceneNodeβidcan be a real node ID OR a library asset key (componentKeyfromsearch_design_system); the import is queued in the plan automatically.$fig.query(selector, scope?)returns{ length, values(paths), first(), last(), each(fn), filter(fn), set(props), moveTo(parent, idx?), remove() }. Selectors are CSS-like (e.g.'FRAME[name*=Card] TEXT').$fig.getStyle(nameOrIdOrKey)and$fig.getVar(nameOrIdOrKey)accept the matchingkeyvalues fromsearch_design_system. - Mutate:
$fig.set(target, props),.delete(...nodes),.move(target, parent, idx?),.clone(target, props?),.append(parent, child),.addAt(parent, idx, child),.replace(old, new),.reorder(parent, children),.gradient(node, type, stops, transform?),.image(node, hash, scaleMode?). - Plan-node methods (chainable):
.set(),.remove(),.clone(),.moveTo(parent, idx?),.append(child),.query(selector),.screenshot({scale?, contentsOnly?}), and the.nodegetter for the materializedSceneNode(null pre-flush).
When to use the raw Figma Plugin API
Only these cases β and even then, mix raw API with $fig in the same script:
- Mid-script async result needed:
await figma.setCurrentPageAsync(...),await figma.loadFontAsync(...)β must complete before subsequent plan steps can use the result. (Importing library components is NOT one of these cases: pass thecomponentKeystraight into$fig.get(...)/$fig.instance(...)and pass variant property values inprops.$figqueues the library import in the plan and resolves the variant for you.) - Mid-script real node state read: measured
width/heightafter auto-layout, computed colors, getStyledTextSegments β materialize mid-script, then read.nodeon the plan node. See references/fig-builder.md for the mid-script inspection pattern. - Things
$figgenuinely doesn't expose:node.setRangeFontName(...), etc. β access viaplanNode.node(see references/fig-builder.md).
Critical Rules
Only use
$figfor creating / bulk-editing nodes ($fig.autoLayout(...),$fig.text(...),$fig.query(...).set(...)). Raw Plugin API is the fallback β use it only when$figcan't express the operation (intermediate node-state reads, non-SceneNode types like Variables). Library components are NOT a reason to leave$fig:$fig.get(componentKey)/$fig.instance(componentSetKey, { props })queue the library import and resolve the variant for you. See references/fig-builder.md and references/critical-rules-deep.md for worked WRONG/RIGHT examples.Do not use
findOne,findAll,findAllWithCriteria,findChildren,findChilddirectly for node searching They are more verbose, error-prone, and less efficient thanquery(). Additionally, do not use recursion to search.Avoid
return/$fig.done()if only using$figβ runtime auto-flushes and returns aFigDoneResultwith created/updated node IDs. Usereturnif you need raw plugin API mid-script or other data.Build up larger designs incrementally by section. Refer to the figma-generate-design skill for the placeholder + replace workflow. Create screens with placeholders inside, e.g.
$fig.autoLayout({ name: 'Header', layoutSizingHorizontal: 'FILL', placeholder: true }), then make subsequentuse_figmacalls to replace them and screenshot:$fig.get("PLACEHOLDER_ID_FROM_PREVIOUS_STEP").replace( ... ).screenshot(). You can make up to 5.screenshot()calls per tool call. If you need to make more screenshots, you are doing too much work and need to break down the task into multipleuse_figmacalls.Plain JS with top-level
await. Code is auto-wrapped in async. Do NOT wrap in(async () => {})().Colors are 0β1 RGB; ALL fields required.
{r, g, b}β nohex:, noa:in color. Opacity goes outside color:{type:'SOLID', color:{r,g,b}, opacity: 0.5}. Hex helper:const hex = h => { const n = parseInt(h.replace('#',''), 16); return { r:((n>>16)&255)/255, g:((n>>8)&255)/255, b:(n&255)/255 } }. See references/critical-rules-deep.md for WRONG/RIGHT.No
curl/wget/Readof Figma URLs fromBash. Figma file access ONLY viause_figmaandmcp__figma__*tools. Afterget_screenshot, the image is inlined in the tool result β do NOT re-fetch or re-Read it.Empty / unsupported responses are terminal. Accept and move on β don't try alternative bypass paths.
Verify node-type before touching a property. Only
FRAME / COMPONENT / COMPONENT_SET / INSTANCE / GROUP / SECTION / PAGEhave.children.GROUPhas nofills/strokes/cornerRadius.TEXThas nocornerRadius/padding*/layoutMode.layoutPositioning='ABSOLUTE'needs parent withlayoutMode !== 'NONE'. Check'<prop>' in nodeor grep references/plugin-api-standalone.d.ts. Full list in references/critical-rules-deep.md.Don't re-query the same info.
get_metadata/get_commentson the same target twice yields the same result. Cache mentally.Be decisive once you have enough info. Don't keep gathering β the marginal information from a 3rd screenshot or 4th metadata call is near-zero.
"An unexpected error occurred" from
use_figmais server-side, not your script bug. Don't retry unchanged β change approach (smaller batch, different selector, drop one node-prop).NEVER call
mcp__figma__get_design_context. FORBIDDEN. This tool requires a selection (no selection exists in this environment), so every call will fail. The error is not recoverable β calling it just burns a tool slot and forces a retry. For structured reads of the file, usemcp__figma__get_metadata(for top-level frame discovery) anduse_figmawith$fig.query()instead. Never callget_design_contextfor any reason.β€3 codebase
Readcalls when the task references source code. Beyond that, grep only. The 4th codebase Read is forbidden β write youruse_figmascript with what you have.3 retries of the same error β switch approach. Most often: switch to
$fig(which handles ordering automatically). Patching the raw API isn't working.Must use auto-layout unless you have a compelling reason not to. Create auto-layout frames with
$fig.autoLayout(...)instead of absolutely-positioning nodes. New auto-layout frames are created with both axes hugging content. Explicitly assignlayoutSizingHorizontalorlayoutSizingVerticalto'FILL'for auto-layout children if you want them to fill the auto-layout container's counter axis.Gradient paints need ALL fields:
type,gradientStops,gradientTransform(a[[a,b,tx],[c,d,ty]]matrix). Missing any throws validation error. See references/critical-rules-deep.md.Discover available fonts, esp. for style variations. Use
await figma.listAvailableFontsAsync()to discover available fonts for$fig.text({ fontName: ... }).Set variable
scopesexplicitly when creating variables. The defaultALL_SCOPESpollutes every property picker. Use specific scopes β['FRAME_FILL', 'SHAPE_FILL']for backgrounds,['TEXT_FILL']for text,['GAP']for spacing,['CORNER_RADIUS']for radii; primitives that shouldn't appear in pickers get[]. In$fig, passscopestocolorVar/numVar; in the raw API, setvariable.scopes. See references/variable-patterns.md.
Bulk mutation of existing nodes (swap/update/replace) is COMPLETE in 3 use_figma calls
For tasks like "swap N icons", "update M colors", "replace K instances":
- DISCOVER + MUTATE in one script (find via
figma.currentPage.query(), import any components, mutate via$fig.query(...).each(...), return count). - VERIFY (optional) β read-only
.query()count. - FINAL REPORT in assistant text, no tool call. STOP.
NO 4th call. NO chasing the last 20% of edge cases. If Call 1 errors, fix and redo β that's still your one mutation call. Full template in references/critical-rules-deep.md.
Node property gotchas
Do not guess node properties or assume CSS-like properties. Accessing non-existent properties will throw TypeError: node.foo: no such property 'foo' on TYPE node. Each throw burns a retry. plugin-api-standalone.index.md contains the list of all symbols in the API. Use that file and grep the full API typings in plugin-api-standalone.d.ts for the full definitions.
- Only
FRAME/COMPONENT/COMPONENT_SET/INSTANCE/GROUP/SECTION/PAGEhave.children.RECTANGLE,TEXT,ELLIPSE,POLYGON,STAR,VECTOR,LINE,SLICE,STICKY,SHAPE_WITH_TEXT,STAMP,CONNECTOR,TABLE,WIDGET,EMBED,MEDIAdo NOT. Check'children' in nodebefore accessing a node's children. GROUPhas NOfills/strokes/cornerRadius. Apply paints/radii on the child shapes inside.- Figma auto-layout != CSS flexbox. There is no such thing as margin.
TEXTDOES NOT HAVE container properties. Text has font / size / decoration / fills. Do not use container properties like padding, layout mode, item spacing, etc.INSTANCEdescendants are read-only for structural ops β you cannotappendChild/insertChildinto an instance child. Edit the sourceCOMPONENTor detach first.- **Never use
primaryAxisSizingModeorcounterAxisSizingModeon a node. ** UselayoutSizingHorizontalorlayoutSizingVerticalwith 'FIXED' | 'HUG' | 'FILL'. Use 'FILL' only when the parent has auto-layout. - There is NO
instance.swapMainComponent(...). Useinstance.setProperties({...})with the component-property variant value, OR$fig.query(...).set({props: {...}}). There ISinstance.swapComponent(component)(different method name).
References
- references/fig-builder.md β full
$figAPI and worked patterns - references/critical-rules-deep.md β WRONG/RIGHT examples, full node-type pitfall list, bulk-mutation template, syntax-bug checklist
- references/gotchas.md β raw Plugin API edge cases
- references/plugin-api-standalone.d.ts β type definitions (grep, don't read whole)
- references/plugin-api-standalone.index.md β API navigation
- references/common-patterns.md, component-patterns.md, variable-patterns.md, text-style-patterns.md, effect-style-patterns.md β pattern playbooks
- references/working-with-design-systems/ β design system workflows
- references/validation-and-recovery.md β error recovery patterns