.agents/skills/figma-use/references/plugin-api-patterns.md
Part of the use_figma skill. Quick reference for common Figma Plugin API operations.
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 populated
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.
Don't build an entire screen in one call. Break work into small steps:
Verify structure with get_metadata between steps. Use get_screenshot after each major creation milestone to catch visual problems early.
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 } }];
// MUST load font before any text operations
await figma.loadFontAsync({ family: "Inter", style: "Regular" });
const text = figma.createText();
text.fontName = { family: "Inter", style: "Regular" };
text.fontSize = 16;
text.lineHeight = { value: 24, unit: "PIXELS" };
text.letterSpacing = { value: 0, unit: "PERCENT" };
text.characters = "Hello World";
text.fills = [{ type: "SOLID", color: { r: 0.1, g: 0.1, b: 0.12 } }];
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 } }];
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 } }];
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;
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);
node.fills = [{ type: "SOLID", color: { r: 0.2, g: 0.2, b: 0.25 } }];
node.fills = [{ type: "SOLID", color: { r: 0.2, g: 0.2, b: 0.25 }, opacity: 0.5 }];
node.fills = [];
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]]
}];
node.strokes = [{ type: "SOLID", color: { r: 0.85, g: 0.85, b: 0.87 } }];
node.strokeWeight = 1;
node.strokeAlign = "INSIDE"; // "CENTER", "OUTSIDE"
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 }
];
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 axis
CRITICAL 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.
// 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'
// 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";
frame.layoutMode = "HORIZONTAL";
frame.layoutWrap = "WRAP";
frame.itemSpacing = 24; // Horizontal gap
frame.counterAxisSpacing = 24; // Vertical gap (between rows)
child.layoutPositioning = "ABSOLUTE";
child.constraints = { horizontal: "MAX", vertical: "MIN" }; // Top-right
child.x = parentWidth - childWidth - 8;
child.y = 8;
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"
}];
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"
}];
node.effects = [{
type: "BACKGROUND_BLUR",
radius: 16,
visible: true
}];
node.effects = [{
type: "LAYER_BLUR",
radius: 8,
visible: true
}];
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" }
];
node.opacity = 0.5;
node.blendMode = "NORMAL"; // "MULTIPLY", "SCREEN", "OVERLAY", "DARKEN", "LIGHTEN", etc.
// Uniform
node.cornerRadius = 12;
// Per-corner
node.topLeftRadius = 12;
node.topRightRadius = 12;
node.bottomLeftRadius = 0;
node.bottomRightRadius = 0;
frame.clipsContent = true; // Children clipped to frame bounds
const group = figma.group([node1, node2, node3], figma.currentPage);
group.name = "Grouped Elements";
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 content
parentFrame.appendChild(childNode);
// Insert at a specific index
parentFrame.insertChild(0, childNode); // Insert at beginning
const component = figma.createComponent();
component.name = "Button/Primary";
component.description = "Primary action button.";
const instance = component.createInstance();
instance.x = 200;
instance.y = 100;
These methods import components from team libraries (not the same file). For components in the current file, use figma.getNodeByIdAsync() or findOne()/findAll().
// Import a published component from a team library by its key
const comp = await figma.importComponentByKeyAsync(componentKey)
const instance = comp.createInstance()
// Import a published component set from a team library by its key
const set = await figma.importComponentSetByKeyAsync(componentSetKey)
const variant = set.defaultVariant
const variantInstance = variant.createInstance()
// 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);
// 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
};
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;
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;
const clone = originalNode.clone();
clone.x = originalNode.x + originalNode.width + 40;
clone.name = "Copy of " + originalNode.name;
// Find by name on current page
const node = figma.currentPage.findOne(n => n.name === "My Frame");
// Find all by type
const allTexts = figma.currentPage.findAll(n => n.type === "TEXT");
// Find all by name pattern
const allButtons = figma.currentPage.findAll(n => n.name.startsWith("Button/"));
frame.layoutGrids = [
{
pattern: "COLUMNS",
alignment: "STRETCH",
count: 12,
gutterSize: 24,
offset: 80,
visible: true
}
];
child.constraints = {
horizontal: "LEFT_RIGHT", // LEFT, RIGHT, CENTER, LEFT_RIGHT, SCALE
vertical: "TOP" // TOP, BOTTOM, CENTER, TOP_BOTTOM, SCALE
};
// Zoom to fit specific nodes
figma.viewport.scrollAndZoomIntoView([frame1, frame2]);