Gotchas & Common Mistakes
Part of the use_figma skill. Every known pitfall with WRONG/CORRECT code examples.
Contents
- Component properties and variant creation pitfalls
- Paint, color, and variable binding pitfalls
- Page context and plugin lifecycle pitfalls
- Auto Layout and sizing order pitfalls (including HUG/FILL interactions)
- Variant layout and geometry pitfalls
- Variable scopes and mode pitfalls
- Node cleanup and empty-fill pitfalls
- detachInstance() and node ID invalidation
New nodes default to (0,0) and overlap existing content
Every figma.create*() call places the node at position (0,0). If you append multiple nodes directly to the page, they all stack on top of each other and on top of any existing content.
This only matters for nodes appended directly to the page (i.e., top-level nodes). Nodes appended as children of other frames, components, or auto-layout containers are positioned by their parent — don't scan for overlaps when nesting nodes.
// WRONG — top-level node lands at (0,0), overlapping existing page content
const frame = figma.createFrame()
frame.name = "My New Frame"
frame.resize(400, 300)
figma.currentPage.appendChild(frame)
// CORRECT — find existing content bounds and place the new top-level node to the right
const page = figma.currentPage
let maxX = 0
for (const child of page.children) {
const right = child.x + child.width
if (right > maxX) maxX = right
}
const frame = figma.createFrame()
frame.name = "My New Frame"
frame.resize(400, 300)
figma.currentPage.appendChild(frame)
frame.x = maxX + 100 // 100px gap from rightmost existing content
frame.y = 0
// NOT NEEDED — child nodes inside a parent don't need overlap scanning
const card = figma.createFrame()
card.layoutMode = 'VERTICAL'
const label = figma.createText()
card.appendChild(label) // positioned by auto-layout, no x/y neededaddComponentProperty returns a string key, not an object — never hardcode or guess it
Figma generates the property key dynamically (e.g. "label#4:0"). The suffix is unpredictable. Always capture and use the return value directly.
// WRONG — guessing / hardcoding the key
comp.addComponentProperty('label', 'TEXT', 'Button')
labelNode.componentPropertyReferences = { characters: 'label#0:1' } // Error: key not found
// WRONG — treating the return value as an object
const result = comp.addComponentProperty('Label', 'TEXT', 'Button')
const propKey = Object.keys(result)[0] // BUG: returns '0' (first char index of string!)
labelNode.componentPropertyReferences = { characters: propKey } // Error: property '0' not found
// CORRECT — the return value IS the key string, use it directly
const propKey = comp.addComponentProperty('Label', 'TEXT', 'Button')
// propKey === "label#4:0" (exact value varies; never assume it)
labelNode.componentPropertyReferences = { characters: propKey }The same applies to COMPONENT_SET nodes — addComponentProperty always returns the property key as a string.
MUST return ALL created/mutated node IDs
Every script that creates or mutates nodes on the canvas must track and return all affected node IDs in the return value. Without these IDs, subsequent calls cannot reference, validate, or clean up those nodes.
// WRONG — only returns the parent frame ID, loses track of children
const frame = figma.createFrame()
const rect = figma.createRectangle()
const text = figma.createText()
frame.appendChild(rect)
frame.appendChild(text)
return { nodeId: frame.id }
// CORRECT — returns all created node IDs in a structured response
const frame = figma.createFrame()
const rect = figma.createRectangle()
const text = figma.createText()
frame.appendChild(rect)
frame.appendChild(text)
return {
createdNodeIds: [frame.id, rect.id, text.id],
rootNodeId: frame.id
}
// CORRECT — when mutating existing nodes, return those IDs too
const nodes = figma.currentPage.findAll(n => n.name === 'Card')
for (const n of nodes) {
n.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]
}
return {
mutatedNodeIds: nodes.map(n => n.id),
count: nodes.length
}Colors are 0–1 range
// WRONG — will throw validation error (ZeroToOne enforced)
node.fills = [{ type: 'SOLID', color: { r: 255, g: 0, b: 0 } }]
// CORRECT
node.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]Fills/strokes are immutable arrays
// WRONG — modifying in place does nothing
node.fills[0].color = { r: 1, g: 0, b: 0 }
// CORRECT — clone, modify, reassign
const fills = JSON.parse(JSON.stringify(node.fills))
fills[0].color = { r: 1, g: 0, b: 0 }
node.fills = fillssetBoundVariableForPaint returns a NEW paint
// WRONG — ignoring return value
figma.variables.setBoundVariableForPaint(paint, "color", colorVar)
node.fills = [paint] // paint is unchanged!
// CORRECT — capture the returned new paint
const boundPaint = figma.variables.setBoundVariableForPaint(paint, "color", colorVar)
node.fills = [boundPaint]Variable collection starts with 1 mode
// A new collection already has one mode — rename it, don't try to add first
const collection = figma.variables.createVariableCollection("Colors")
// collection.modes = [{ modeId: "...", name: "Mode 1" }]
collection.renameMode(collection.modes[0].modeId, "Light")
const darkModeId = collection.addMode("Dark")combineAsVariants requires ComponentNodes
// WRONG — passing frames
const f1 = figma.createFrame()
figma.combineAsVariants([f1], figma.currentPage) // Error!
// CORRECT — passing components
const c1 = figma.createComponent()
c1.name = "variant=primary, size=md"
const c2 = figma.createComponent()
c2.name = "variant=secondary, size=md"
figma.combineAsVariants([c1, c2], figma.currentPage)Page switching: sync setter throws
The sync setter figma.currentPage = page throws an error in use_figma runtimes (MCP, evals, assistant). Use await figma.setCurrentPageAsync(page) instead — it switches the page and loads its content.
// WRONG — throws "Setting figma.currentPage is not supported in this runtime"
figma.currentPage = targetPage
// CORRECT — async method switches and loads content
await figma.setCurrentPageAsync(targetPage)get_metadata only sees one page — use use_figma to discover all pages
A Figma file can have multiple pages (canvas nodes). get_metadata operates on a single node/page — it cannot scan the entire document. To discover all pages and their top-level contents, use use_figma:
// WRONG — calling get_metadata with the file root or expecting it to list all pages
// get_metadata only returns the subtree of the node you pass it
// CORRECT — use use_figma to list pages, then inspect each one
const pages = figma.root.children.map(p => `${p.name} id=${p.id} children=${p.children.length}`);
return pages.join('\n');Icons, variables, and components may live on pages other than the first. Always enumerate all pages before concluding that the file has no existing assets.
Never use figma.notify()
// WRONG — throws "not implemented" error
figma.notify("Done!")
// CORRECT — return a value to send data back to the agent
return "Done!"getPluginData() / setPluginData() are not supported
These APIs are not available in the use_figma runtime. Use getSharedPluginData() / setSharedPluginData() instead (these ARE supported), or track nodes by returning IDs.
// WRONG — not supported in use_figma
node.setPluginData('my_key', 'my_value')
const val = node.getPluginData('my_key')
// CORRECT — use shared plugin data (requires a namespace)
node.setSharedPluginData('my_namespace', 'my_key', 'my_value')
const val = node.getSharedPluginData('my_namespace', 'my_key')
// ALSO CORRECT — return node IDs and track them across calls
const rect = figma.createRectangle()
return { nodeId: rect.id }
// Then pass nodeId as a string literal in the next use_figma callScript must always return a value
// WRONG — no return, caller gets no useful response
figma.createRectangle()
// CORRECT — return a result (objects are auto-serialized, errors are auto-captured)
const rect = figma.createRectangle()
return { nodeId: rect.id }setBoundVariable for paint fields only works on SOLID paints
// Only SOLID paint type supports color variable binding
// Gradient paints, image paints, etc. will throw
const solidPaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }
const bound = figma.variables.setBoundVariableForPaint(solidPaint, "color", colorVar)Explicit variable modes must be set per component
// WRONG — all variants render with the default (first) mode
const colorCollection = figma.variables.createVariableCollection("Colors")
// ... create variables and modes ...
// Components all show the first mode's values by default!
// CORRECT — set explicit mode on each component to get variant-specific values
component.setExplicitVariableModeForCollection(colorCollection, targetModeId)TextStyle.setBoundVariable is not available in headless use_figma
setBoundVariable exists on TextStyle in the typed API but is not available when running scripts through use_figma (MCP, headless assistant mode). Calling it will throw "not a function".
// WRONG — throws "not a function" in use_figma / headless
const ts = figma.createTextStyle()
ts.setBoundVariable("fontSize", fontSizeVar)
// CORRECT (headless) — set raw values; bind variables interactively in Figma later
const ts = figma.createTextStyle()
ts.fontSize = 24This only affects TextStyle. Variable binding on nodes (node.setBoundVariable(...)) and on paint objects (figma.variables.setBoundVariableForPaint(...)) still works in headless mode as expected.
If live variable binding on text styles is required, create the styles with raw values via use_figma, then bind variables interactively through the Figma Styles panel or a full interactive plugin.
lineHeight and letterSpacing must be objects, not bare numbers
// WRONG — throws or silently does nothing
style.lineHeight = 1.5
style.lineHeight = 24
style.letterSpacing = 0
// CORRECT
style.lineHeight = { unit: "AUTO" } // auto/intrinsic
style.lineHeight = { value: 24, unit: "PIXELS" } // fixed pixel height
style.lineHeight = { value: 150, unit: "PERCENT" } // percentage of font size
style.letterSpacing = { value: 0, unit: "PIXELS" } // no tracking
style.letterSpacing = { value: -0.5, unit: "PIXELS" } // tight
style.letterSpacing = { value: 5, unit: "PERCENT" } // percent-basedThis applies to both TextStyle and TextNode properties. The same rule applies inside use_figma, interactive plugins, and any other plugin API context.
Font style names are file-dependent — probe before assuming
Font style names vary per provider and per Figma file. "SemiBold" and "Semi Bold" are different strings. Loading a font with the wrong style string throws silently or errors — there is no canonical list.
// WRONG — guessing style names
await figma.loadFontAsync({ family: "Inter", style: "SemiBold" }) // may throw
// CORRECT — probe which style names are available
const candidates = ["SemiBold", "Semi Bold", "Semibold"]
for (const style of candidates) {
try {
await figma.loadFontAsync({ family: "Inter", style })
// capture the one that works
break
} catch (_) {}
}When building a type ramp script, always verify font styles against the target file before hardcoding them.
combineAsVariants does NOT auto-layout in headless mode
// WRONG — all variants stack at position (0, 0), resulting in a tiny ComponentSet
const components = [comp1, comp2, comp3]
const cs = figma.combineAsVariants(components, figma.currentPage)
// cs.width/height will be the size of a SINGLE variant!
// CORRECT — manually layout children in a grid after combining
const cs = figma.combineAsVariants(components, figma.currentPage)
const colWidth = 120
const rowHeight = 56
cs.children.forEach((child, i) => {
const col = i % numCols
const row = Math.floor(i / numCols)
child.x = col * colWidth
child.y = row * rowHeight
})
// CRITICAL: resize from actual child bounds, not formula — formula errors leave variants outside the boundary
let maxX = 0, maxY = 0
for (const child of cs.children) {
maxX = Math.max(maxX, child.x + child.width)
maxY = Math.max(maxY, child.y + child.height)
}
cs.resizeWithoutConstraints(maxX + 40, maxY + 40)COLOR variable values use {r, g, b, a} (with alpha)
// Paint colors use {r, g, b} (no alpha — opacity is a separate paint property)
node.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]
// But COLOR variable values use {r, g, b, a} — alpha maps to paint opacity
const colorVar = figma.variables.createVariable("bg", collection, "COLOR")
colorVar.setValueForMode(modeId, { r: 1, g: 0, b: 0, a: 1 }) // opaque red
colorVar.setValueForMode(modeId, { r: 0, g: 0, b: 0, a: 0 }) // fully transparent
// ⚠️ Don't confuse: {r, g, b} for paint colors vs {r, g, b, a} for variable valueslayoutSizingVertical/layoutSizingHorizontal = 'FILL' requires auto-layout parent FIRST
// WRONG — setting FILL before the node is a child of an auto-layout frame
const child = figma.createFrame()
child.layoutSizingVertical = 'FILL' // ERROR: "FILL can only be set on children of auto-layout frames"
parent.appendChild(child)
// CORRECT — append to auto-layout parent FIRST, then set FILL
const child = figma.createFrame()
parent.appendChild(child) // parent must have layoutMode set
child.layoutSizingVertical = 'FILL' // Works!HUG parents collapse FILL children
A HUG parent cannot give FILL children meaningful size. If children have layoutSizingHorizontal = "FILL" but the parent is "HUG", the children collapse to minimum size. The parent must be "FILL" or "FIXED" for FILL children to expand. This is a common cause of truncated text in select fields, inputs, and action rows.
// WRONG — parent hugs, so FILL children get zero extra space
const parent = figma.createFrame()
parent.layoutMode = 'HORIZONTAL'
parent.layoutSizingHorizontal = 'HUG'
const child = figma.createFrame()
parent.appendChild(child)
child.layoutSizingHorizontal = 'FILL' // collapses to min size!
// CORRECT — parent must be FIXED or FILL for FILL children to expand
const parent = figma.createFrame()
parent.layoutMode = 'HORIZONTAL'
parent.resize(400, 50)
parent.layoutSizingHorizontal = 'FIXED' // or 'FILL' if inside another auto-layout
const child = figma.createFrame()
parent.appendChild(child)
child.layoutSizingHorizontal = 'FILL' // expands to fill remaining 400pxlayoutGrow with a hugging parent causes content compression
// WRONG — layoutGrow on a child when parent has primaryAxisSizingMode='AUTO' (hug)
// causes the child to SHRINK below its natural size instead of expanding
const parent = figma.createComponent()
parent.layoutMode = 'VERTICAL'
parent.primaryAxisSizingMode = 'AUTO' // hug contents
const content = figma.createFrame()
content.layoutMode = 'VERTICAL'
content.primaryAxisSizingMode = 'AUTO'
parent.appendChild(content)
content.layoutGrow = 1 // BUG: content compresses, children hidden!
// CORRECT — only use layoutGrow when parent has FIXED sizing with extra space
content.layoutGrow = 0 // let content take its natural size
// OR: set parent to FIXED sizing first
parent.primaryAxisSizingMode = 'FIXED'
parent.resizeWithoutConstraints(300, 500)
content.layoutGrow = 1 // NOW it correctly fills remaining spaceresize() resets primaryAxisSizingMode and counterAxisSizingMode to FIXED
resize(w, h) silently resets both sizing modes to FIXED. If you call it after setting HUG, the frame locks to the exact pixel value you passed — even a throwaway like 1.
// WRONG — resize() after setting sizing mode overwrites it