> Part of the [figma-generate-library skill](../SKILL.md). # Component Creation Reference Complete guide for Phase 3: building components with variant matrices, variable bindings, component properties, and documentation. --- ## 1. Component Architecture ### Dependency Ordering: Atoms Before Molecules Always build in dependency order. A molecule that contains an atom instance cannot exist until the atom is published. Suggested ordering: ``` Tier 0 (atoms): Icon, Avatar, Badge, Spinner Tier 1 (molecules): Button, Checkbox, Toggle, Input, Select Tier 2 (organisms): Card, Dialog, Menu, Navigation, Form ``` If a component embeds an instance of another component, the embedded component must be created first. Build your dependency graph during Phase 0 and encode the creation order in the plan. ### Building Blocks Sub-Components (M3 Pattern) For complex components with independent sub-element state machines, extract the sub-element into its own component set prefixed with `Building Blocks/` (public) or `.Building Blocks/` (hidden from assets panel). The dot-prefix is a Figma convention for suppressing a component from the public assets panel. **When to use Building Blocks:** - The sub-element has its own variant axes (state, selection) that would cause combinatorial explosion in the parent - The sub-element repeats (nav items, table cells, calendar cells, segmented button segments) - The sub-element has different variant axes than the parent **Example (M3 Segmented Button):** ``` Building Blocks/Segmented button/Button segment (start) [27 variants: Config × State × Selected] Building Blocks/Segmented button/Button segment (middle) [27 variants] Building Blocks/Segmented button/Button segment (end) [27 variants] Segmented button [16 variants: Segments=2-5 × Density=0/-1/-2/-3] Each variant contains instances of the appropriate Building Block segment components. ``` The parent manages composition and configuration; the Building Block manages its own interaction states. ### Private Components (`__` Prefix) Use the `__` prefix for internal helper components that should not appear in the team library (Shop Minis pattern). Use `_` for documentation-only components (UI3 pattern). ``` __asset // private icon/asset holder _Label/Direction // documentation annotation helper ``` --- ## 2. Creating the Component Page Each component lives on its own dedicated page (one page per component is the default). The page contains: a documentation frame at top-left and the component set positioned to its right or below. ```javascript // Create or find the component page let page = figma.root.children.find(p => p.name === 'Button'); if (!page) { page = figma.createPage(); page.name = 'Button'; } await figma.setCurrentPageAsync(page); // Documentation frame — positioned at (40, 40) const docFrame = figma.createFrame(); docFrame.name = 'Button / Documentation'; docFrame.x = 40; docFrame.y = 40; docFrame.resize(600, 400); docFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]; docFrame.layoutMode = 'VERTICAL'; docFrame.primaryAxisSizingMode = 'AUTO'; docFrame.counterAxisSizingMode = 'FIXED'; docFrame.paddingTop = 40; docFrame.paddingBottom = 40; docFrame.paddingLeft = 40; docFrame.paddingRight = 40; docFrame.itemSpacing = 16; // Title text node await figma.loadFontAsync({ family: 'Inter', style: 'Bold' }); const title = figma.createText(); title.fontName = { family: 'Inter', style: 'Bold' }; title.fontSize = 32; title.characters = 'Button'; docFrame.appendChild(title); // Description text node await figma.loadFontAsync({ family: 'Inter', style: 'Regular' }); const desc = figma.createText(); desc.fontName = { family: 'Inter', style: 'Regular' }; desc.fontSize = 14; desc.characters = 'Buttons allow users to take actions and make choices with a single tap.'; docFrame.appendChild(desc); // Tag docFrame with sharedPluginData for idempotency docFrame.setSharedPluginData('dsb', 'run_id', RUN_ID); docFrame.setSharedPluginData('dsb', 'key', 'doc/button'); return { docFrameId: docFrame.id, pageId: page.id }; ``` --- ## 3. Base Component: Auto-Layout, Child Nodes, Variable Bindings The base component is the template from which all variants are cloned. It must have: 1. Auto-layout (not manual positioning) 2. All child nodes present 3. ALL visual properties bound to variables (no hardcoded values) ### Complete Button Base Component Example ```javascript const RUN_ID = 'ds-build-2024-001'; // replace with your actual run ID await figma.setCurrentPageAsync( figma.root.children.find(p => p.name === 'Button') ); // Rehydrate variables from IDs stored in state ledger const bgVar = await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_primary'); const textVar = await figma.variables.getVariableByIdAsync('VAR_ID_color_text_on_primary'); const paddingVar = await figma.variables.getVariableByIdAsync('VAR_ID_spacing_md'); const radiusVar = await figma.variables.getVariableByIdAsync('VAR_ID_radius_md'); const gapVar = await figma.variables.getVariableByIdAsync('VAR_ID_spacing_sm'); // --- Base component frame --- const comp = figma.createComponent(); comp.name = 'Size=Medium, Style=Primary, State=Default'; comp.layoutMode = 'HORIZONTAL'; comp.primaryAxisSizingMode = 'AUTO'; comp.counterAxisSizingMode = 'AUTO'; comp.counterAxisAlignItems = 'CENTER'; comp.primaryAxisAlignItems = 'CENTER'; // Padding — bound to spacing variables comp.setBoundVariable('paddingTop', paddingVar); comp.setBoundVariable('paddingBottom', paddingVar); comp.setBoundVariable('paddingLeft', paddingVar); comp.setBoundVariable('paddingRight', paddingVar); comp.setBoundVariable('itemSpacing', gapVar); // Corner radius — bound to radius variable comp.setBoundVariable('topLeftRadius', radiusVar); comp.setBoundVariable('topRightRadius', radiusVar); comp.setBoundVariable('bottomLeftRadius', radiusVar); comp.setBoundVariable('bottomRightRadius', radiusVar); // Background fill — bound to color variable const bgPaint = figma.variables.setBoundVariableForPaint( { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', bgVar ); comp.fills = [bgPaint]; // --- Label text node --- await figma.loadFontAsync({ family: 'Inter', style: 'Medium' }); const label = figma.createText(); label.name = 'label'; label.fontName = { family: 'Inter', style: 'Medium' }; label.fontSize = 14; label.characters = 'Button'; label.layoutSizingHorizontal = 'HUG'; label.layoutSizingVertical = 'HUG'; // Text fill — bound to color variable const textPaint = figma.variables.setBoundVariableForPaint( { type: 'SOLID', color: { r: 1, g: 1, b: 1 } }, 'color', textVar ); label.fills = [textPaint]; comp.appendChild(label); // --- Icon placeholder (Rectangle for now — will be INSTANCE_SWAP) --- const iconBox = figma.createFrame(); iconBox.name = 'icon'; iconBox.resize(16, 16); iconBox.fills = []; iconBox.layoutSizingHorizontal = 'FIXED'; iconBox.layoutSizingVertical = 'FIXED'; comp.appendChild(iconBox); // Tag for idempotency comp.setSharedPluginData('dsb', 'run_id', RUN_ID); comp.setSharedPluginData('dsb', 'phase', 'phase3'); comp.setSharedPluginData('dsb', 'key', 'component/button/base'); return { baseCompId: comp.id }; ``` **ALL of these must be variable-bound (never hardcoded):** | Property | Variable type | API method | |---|---|---| | Fill color | COLOR | `setBoundVariableForPaint(..., 'color', var)` | | Stroke color | COLOR | `setBoundVariableForPaint(..., 'color', var)` | | Text fill | COLOR | `setBoundVariableForPaint(..., 'color', var)` | | Padding (all 4 sides) | FLOAT | `comp.setBoundVariable('paddingTop', var)` | | Gap / itemSpacing | FLOAT | `comp.setBoundVariable('itemSpacing', var)` | | Corner radius (all 4) | FLOAT | `comp.setBoundVariable('topLeftRadius', var)` etc. | | Stroke weight | FLOAT | `comp.setBoundVariable('strokeWeight', var)` | --- ## 4. Variant Matrix ### Defining Axes For each component, identify its variant axes before writing any code. Standard axes: ``` Button: Size → [Small, Medium, Large] Style → [Primary, Secondary, Outline, Ghost] State → [Default, Hover, Focused, Pressed, Disabled] Total = 3 × 4 × 5 = 60 combinations — exceeds 30 limit → split by Style ``` ### The 30-Combination Cap and Split Strategy When the product of all variant axes exceeds 30 combinations, split the matrix. Options: 1. **Split by a primary axis**: Create separate component sets, one per Style (Primary Button, Secondary Button, etc.) 2. **Use INSTANCE_SWAP**: Remove a visual axis (like Icon) from the variant matrix entirely and expose it as an INSTANCE_SWAP property instead 3. **Use Building Blocks**: Extract sub-elements with their own state axes into Building Block component sets For Button with Size × State = 15 combinations, add Style as a variant axis only if Style ≤ 2 options (15 × 2 = 30). For more Styles, split. ### Creating All Variants with use_figma Build each variant by cloning the base component and adjusting the variable bindings that differ per variant. Pass in the base component ID from the previous call's state. ```javascript const RUN_ID = 'ds-build-2024-001'; const BASE_COMP_ID = 'BASE_ID_FROM_STATE'; // from state ledger await figma.setCurrentPageAsync( figma.root.children.find(p => p.name === 'Button') ); const base = await figma.getNodeByIdAsync(BASE_COMP_ID); // Variable IDs from state ledger const vars = { // Primary style bg_primary: await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_primary'), text_primary: await figma.variables.getVariableByIdAsync('VAR_ID_color_text_on_primary'), // Secondary style bg_secondary: await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_secondary'), text_secondary: await figma.variables.getVariableByIdAsync('VAR_ID_color_text_secondary'), // Disabled bg_disabled: await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_disabled'), text_disabled: await figma.variables.getVariableByIdAsync('VAR_ID_color_text_disabled'), // Sizes padding_sm: await figma.variables.getVariableByIdAsync('VAR_ID_spacing_sm'), padding_md: await figma.variables.getVariableByIdAsync('VAR_ID_spacing_md'), padding_lg: await figma.variables.getVariableByIdAsync('VAR_ID_spacing_lg'), }; const axes = { Size: ['Small', 'Medium', 'Large'], Style: ['Primary', 'Secondary'], State: ['Default', 'Hover', 'Disabled'], }; const paddingBySize = { Small: vars.padding_sm, Medium: vars.padding_md, Large: vars.padding_lg }; const components = []; for (const size of axes.Size) { for (const style of axes.Style) { for (const state of axes.State) { const clone = base.clone(); clone.name = `Size=${size}, Style=${style}, State=${state}`; // Bind padding by size clone.setBoundVariable('paddingTop', paddingBySize[size]); clone.setBoundVariable('paddingBottom', paddingBySize[size]); clone.setBoundVariable('paddingLeft', paddingBySize[size]); clone.setBoundVariable('paddingRight', paddingBySize[size]); // Bind fill by style + state const isDisabled = state === 'Disabled'; const bgVar = isDisabled ? vars.bg_disabled : (style === 'Primary' ? vars.bg_primary : vars.bg_secondary); const txtVar = isDisabled ? vars.text_disabled : (style === 'Primary' ? vars.text_primary : vars.text_secondary); const bgPaint = figma.variables.setBoundVariableForPaint( { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', bgVar ); clone.fills = [bgPaint]; const labelNode = clone.findOne(n => n.name === 'label'); const textPaint = figma.variables.setBoundVariableForPaint( { type: 'SOLID', color: { r: 1, g: 1, b: 1 } }, 'color', txtVar ); labelNode.fills = [textPaint]; clone.setSharedPluginData('dsb', 'run_id', RUN_ID); clone.setSharedPluginData('dsb', 'key', `component/button/variant/${size}/${style}/${state}`); components.push(clone); } } } return { variantIds: components.map(c => c.id) }; ``` --- ## 5. `combineAsVariants` + Grid Layout After all variant components exist, combine them into a ComponentSet and position them in a grid. This MUST be a separate `use_figma` call — you must pass in all variant IDs from the previous call's return value. ### Grid Design Conventions Professional design systems lay out variants in a readable grid where: - **Columns** = the property users interact with most (typically **State**: Default, Hover, Focused, Pressed, Disabled) - **Rows** = structural axes grouped together (typically **Size × Style**, where Size varies fastest) - **Gap** = 16–40px between variants (20px is a safe default; match existing file if one exists) - **Padding** = 40px around the grid inside the ComponentSet frame ``` Visual structure: Default Hover Focused Pressed Disabled ┌──────────────────────────────────────────────────────────────────┐ │ Small/Primary [comp] [comp] [comp] [comp] [comp] │ │ Small/Secondary [comp] [comp] [comp] [comp] [comp] │ │ Medium/Primary [comp] [comp] [comp] [comp] [comp] │ │ Medium/Secondary[comp] [comp] [comp] [comp] [comp] │ │ Large/Primary [comp] [comp] [comp] [comp] [comp] │ │ Large/Secondary [comp] [comp] [comp] [comp] [comp] │ └──────────────────────────────────────────────────────────────────┘ ``` **Why State on columns?** State is the axis designers scan horizontally to verify interaction consistency. Size/Style define the "identity" of each row. This matches how professional design systems (M3, Polaris, Simple DS) organize their grids. ### Adding Row/Column Header Labels After laying out the grid, add text labels OUTSIDE the ComponentSet to help navigation. These are siblings of the ComponentSet on the page — not children of it: ```javascript // Add column headers above the component set const colLabels = ['Default', 'Hover', 'Focused', 'Pressed', 'Disabled']; await figma.loadFontAsync({ family: 'Inter', style: 'Medium' }); for (let i = 0; i < colLabels.length; i++) { const label = figma.createText(); label.fontName = { family: 'Inter', style: 'Medium' }; label.characters = colLabels[i]; label.fontSize = 11; label.fills = [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }]; label.x = cs.x + padding + i * (childWidth + gap); label.y = cs.y - 20; } // Add row headers to the left of the component set const rowLabels = ['Small / Primary', 'Small / Secondary', 'Med / Primary', ...]; for (let i = 0; i < rowLabels.length; i++) { const label = figma.createText(); label.fontName = { family: 'Inter', style: 'Medium' }; label.characters = rowLabels[i]; label.fontSize = 11; label.fills = [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }]; label.x = cs.x - 120; label.y = cs.y + padding + i * (childHeight + gap) + childHeight / 2 - 6; } ``` **Note:** These labels are documentation aids, not part of the component itself. They help designers navigate the variant grid. ### Grid layout code ```javascript const VARIANT_IDS = ['ID1', 'ID2', '...']; // from state ledger const PAGE_ID = 'PAGE_ID'; // from state ledger await figma.setCurrentPageAsync(await figma.getNodeByIdAsync(PAGE_ID)); // Collect component nodes const components = await Promise.all( VARIANT_IDS.map(id => figma.getNodeByIdAsync(id)) ); // Combine as variants const cs = figma.combineAsVariants(components, figma.currentPage); cs.name = 'Button'; // Grid layout: position each variant based on its property values // Determine column axis (State) and row axes (Size × Style) const axes = { Size: ['Small', 'Medium', 'Large'], Style: ['Primary', 'Secondary'], State: ['Default', 'Hover', 'Disabled'], }; const COL_AXIS = 'State'; // columns const ROW_AXES = ['Size', 'Style']; // rows (Size changes fastest) const gap = 16; const padding = 40; // Measure child dimensions (all should be same height within Size tier) // Use the first child as reference for column width const childWidth = 120; // approximate; refine after first screenshot const childHeight = 40; cs.children.forEach(child => { const props = {}; child.name.split(', ').forEach(part => { const [k, v] = part.split('='); props[k] = v; }); const colIdx = axes[COL_AXIS].indexOf(props[COL_AXIS]); // Row = Size index * number of styles + Style index const rowIdx = axes.Size.indexOf(props.Size) * axes.Style.length + axes.Style.indexOf(props.Style); child.x = padding + colIdx * (childWidth + gap); child.y = padding + rowIdx * (childHeight + gap); }); // Resize component set to fit all children + padding 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 + padding, maxY + padding); // Style the component set frame cs.fills = [{ type: 'SOLID', color: { r: 0.95, g: 0.95, b: 0.98 } }]; cs.cornerRadius = 8; // Position component set on page (to the right of doc frame) cs.x = 680; cs.y = 40; cs.setSharedPluginData('dsb', 'run_id', 'ds-build-2024-001'); cs.setSharedPluginData('dsb', 'key', 'componentset/button'); return { componentSetId: cs.id }; ``` **Critical rules for combineAsVariants:** - `components` must be a non-empty array containing ONLY `ComponentNode` objects (not frames, not groups) - After combining, children are placed at (0,0) and overlap — you MUST manually position them - `resizeWithoutConstraints` is required after positioning to make the component set frame fit its contents - There is no `figma.createComponentSet()` — you cannot create an empty component set --- ## 6. Component Properties Add TEXT, BOOLEAN, and INSTANCE_SWAP properties to the ComponentSet (not to individual variants). The return value of `addComponentProperty` is the actual property key (it gets a `#id:id` suffix appended) — save this key and use it immediately when setting `componentPropertyReferences`. ### TEXT Properties Expose editable text in instances: ```javascript // On the ComponentSetNode (cs): const labelKey = cs.addComponentProperty('Label', 'TEXT', 'Button'); // labelKey is now something like "Label#0:1" // Wire to the label child in each variant: for (const child of cs.children) { const labelNode = child.findOne(n => n.name === 'label'); if (labelNode) { labelNode.componentPropertyReferences = { characters: labelKey }; } } ``` ### BOOLEAN Properties Toggle child node visibility: ```javascript const showIconKey = cs.addComponentProperty('Show Icon', 'BOOLEAN', true); for (const child of cs.children) { const iconNode = child.findOne(n => n.name === 'icon'); if (iconNode) { iconNode.componentPropertyReferences = { visible: showIconKey }; } } ``` ### INSTANCE_SWAP Properties Allow swapping a nested component instance (e.g., swap the icon): ```javascript // defaultIconCompId is the ID of the default icon component (from state ledger) const iconKey = cs.addComponentProperty('Icon', 'INSTANCE_SWAP', DEFAULT_ICON_COMP_ID); for (const child of cs.children) { const iconSlot = child.findOne(n => n.name === 'icon'); if (iconSlot && iconSlot.type === 'INSTANCE') { iconSlot.componentPropertyReferences = { mainComponent: iconKey }; } } ``` **Use INSTANCE_SWAP instead of creating a variant per icon.** Never add "Icon=ChevronRight, Icon=ChevronLeft, ..." as VARIANT axes — that causes combinatorial explosion. One INSTANCE_SWAP property covers all icons. ### Creating Icon Components for INSTANCE_SWAP INSTANCE_SWAP needs a real Component ID as its default value. Before wiring INSTANCE_SWAP, you need at least one icon component. Here's how to create icons from SVG: ```javascript // Create a simple icon component from SVG const svgNode = figma.createNodeFromSvg( '' + '' + '' ); // Wrap in a component const iconComp = figma.createComponent(); iconComp.name = 'Icon/ChevronRight'; iconComp.resize(24, 24); iconComp.clipsContent = true; // Move SVG children into the component for (const child of [...svgNode.children]) { iconComp.appendChild(child); } svgNode.remove(); // Bind the icon fill to a color variable (so it respects themes) // Find vector children and bind their fills iconComp.findAll(n => n.type === 'VECTOR').forEach(vec => { // For stroke-based icons: if (vec.strokes.length > 0) { const strokePaint = figma.variables.setBoundVariableForPaint( { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', iconColorVar ); vec.strokes = [strokePaint]; } }); iconComp.setSharedPluginData('dsb', 'run_id', RUN_ID); iconComp.setSharedPluginData('dsb', 'key', 'icon/chevron-right'); return { iconCompId: iconComp.id }; ``` **Then use the returned `iconCompId` as the default value for INSTANCE_SWAP:** ```javascript const iconKey = cs.addComponentProperty('Icon', 'INSTANCE_SWAP', ICON_COMP_ID); ``` **Constraining swap options with `preferredValues`:** After adding the INSTANCE_SWAP property, you can optionally limit which components appear in the swap picker: ```javascript // Get the property definitions to find the exact key const props = cs.componentPropertyDefinitions; const iconPropKey = Object.keys(props).find(k => k.startsWith('Icon')); // Set preferred values (array of component keys or instance IDs) cs.editComponentProperty(iconPropKey, { preferredValues: [ { type: 'COMPONENT', key: chevronRightComp.key }, { type: 'COMPONENT', key: chevronLeftComp.key }, { type: 'COMPONENT', key: closeComp.key }, ], }); ``` **Icon library tip:** Create all icon components on a dedicated `Icons` page before building any UI components. Then reference their IDs when wiring INSTANCE_SWAP properties. ### `componentPropertyReferences` mapping The `componentPropertyReferences` object maps a node's own property to a component property key: | Node property | Component property type | Used for | |---|---|---| | `characters` | TEXT | Editable text content | | `visible` | BOOLEAN | Show/hide toggle | | `mainComponent` | INSTANCE_SWAP | Swap nested instances | --- ## 7. `sharedPluginData` Tagging for Idempotency Tag EVERY created node immediately after creation. This enables safe cleanup, resumability, and idempotency checks. ```javascript // After creating any node: node.setSharedPluginData('dsb', 'run_id', RUN_ID); // identifies the build run node.setSharedPluginData('dsb', 'phase', 'phase3'); // which phase created it node.setSharedPluginData('dsb', 'key', KEY); // unique logical key for this entity // Reading back: const runId = node.getSharedPluginData('dsb', 'run_id'); // '' if not set const key = node.getSharedPluginData('dsb', 'key'); ``` **Key naming convention:** use `/`-separated logical paths that mirror the entity hierarchy: ``` 'component/button/base' 'component/button/variant/Medium/Primary/Default' 'componentset/button' 'doc/button' 'page/button' ``` **Idempotency check before creating:** before creating a node, scan the current page for an existing node with the same `key`: ```javascript const existing = figma.currentPage.findAll(n => n.getSharedPluginData('dsb', 'key') === 'componentset/button' ); if (existing.length > 0) { // Skip creation — already done. Return existing node's ID. return { componentSetId: existing[0].id }; } ``` --- ## 8. Documentation ### Page title + description frame The documentation frame (see Section 2) should contain: 1. Component name as a large title (32px+ Bold) 2. 1–3 sentence description of what the component is and when to use it 3. Spec notes (sizes, spacing values, accessibility notes) ### Component `description` property Set the description on the ComponentSet — it appears in the Figma properties panel and is exported as documentation: ```javascript cs.description = 'Buttons allow users to take actions and make choices. Use Primary for the highest-emphasis action on a page.'; ``` ### `documentationLinks` Link to external documentation (Storybook, design spec, tokens reference): ```javascript cs.documentationLinks = [ { uri: 'https://your-storybook.com/button' } ]; ``` ### Node names and organization - ComponentSet: plain component name — `'Button'` - Individual variants: `'Property=Value, Property=Value'` format (match the file's existing casing) - Child nodes: semantic names — `'label'`, `'icon'`, `'container'`, `'state-layer'` - Documentation frames: `'ComponentName / Documentation'` --- ## 9. Validation Always validate after creating or modifying a component before proceeding to the next one. ### `get_metadata` structural checks After creating the component set, call `get_metadata` on the ComponentSet node and verify: - `variantGroupProperties` lists the expected axes with the correct value arrays - `componentPropertyDefinitions` contains the expected TEXT/BOOLEAN/INSTANCE_SWAP properties - `children.length` equals the expected variant count (e.g., 18 for 3×2×3) - No children are named `'Component 1'` (unnamed components are a sign of a bug) ### `get_screenshot` — Visual Validation (Critical) `get_screenshot` returns an **image** of the specified node. Call it on the **component page node** (not the component set) to see the full page including documentation and grid labels. ``` Tool: get_screenshot Args: { nodeId: "PAGE_NODE_ID", fileKey: "FILE_KEY" } ``` **How to use the screenshot:** 1. **Display it to the user** — this is the primary purpose. Show the screenshot as part of the user checkpoint: "Here's the Button component. Does it look right?" 2. **Analyze it yourself** — if you have vision capabilities, check the visual checklist below. If you don't (text-only agent), fall back to structural validation only via `get_metadata` and describe what you created textually. **Visual validation checklist** (check each item when viewing the screenshot): | # | Check | What "good" looks like | What "broken" looks like | |---|-------|----------------------|------------------------| | 1 | **Grid layout** | Variants in neat rows and columns with consistent spacing | All variants piled at top-left (0,0 stacking bug) | | 2 | **Color fills** | Components show distinct, correct colors per style variant | All components are black or same color (variable binding failed) | | 3 | **Size differentiation** | Small variants are visibly smaller than Large variants | All variants are the same size (height/padding not bound to variables) | | 4 | **Text readability** | Labels are visible with correct font and color | Text is invisible (white on white), missing, or shows "undefined" | | 5 | **Spacing/padding** | Interior padding visible, components aren't "shrink-wrapped" | Components look cramped or have no visible internal space | | 6 | **State differentiation** | Hover/Pressed variants have visible color differences from Default | All states look identical (state-specific fills not applied) | | 7 | **Disabled state** | Lower opacity or muted colors compared to active states | Disabled looks identical to Default | | 8 | **Documentation frame** | Title + description text visible above or beside the component grid | No documentation, or it overlaps the component set | | 9 | **Grid labels** | Row/column headers visible around the component set (if added) | Labels overlap the grid or are missing | | 10 | **Component set boundary** | Gray background frame wraps all variants with even padding | Frame is too small (variants clipped) or way too large | **Screenshot → diagnosis → fix mapping:** | Screenshot shows | Diagnosis | Fix script | |-----------------|-----------|------------| | All variants stacked top-left | Grid layout wasn't applied after `combineAsVariants` | Re-run the grid layout script (§5) | | Everything black/same color | Variable bindings failed or variables don't have values for the active mode | Re-run variable binding, check mode values | | No text visible | Font wasn't loaded, or text fill is same color as background | Check `loadFontAsync` was called; bind text fill to `color/text/*` variable | | Variants all same size | Padding/height not bound to size variables | Re-run `bindVariablesToComponent` with size-specific tokens | | Component set frame tiny | `resizeWithoutConstraints` wasn't called or used wrong dimensions | Re-calculate bounds from children and resize | | Doc frame overlaps components | Component set positioned at same x,y as doc frame | Move component set: `cs.x = docFrame.x + docFrame.width + 60` | **When visual analysis isn't available:** If your model can't process images (text-only mode), validate structurally instead: 1. Call `get_metadata` on the component set — verify child count, property definitions, variant names 2. Run an `use_figma` that samples key properties: ```javascript const cs = await figma.getNodeByIdAsync(CS_ID); const sample = cs.children.slice(0, 3).map(c => ({ name: c.name, width: c.width, height: c.height, x: c.x, y: c.y, fills: c.fills?.map(f => f.type === 'SOLID' ? { r: f.color.r.toFixed(2), g: f.color.g.toFixed(2), b: f.color.b.toFixed(2), boundVar: f.boundVariables?.color?.id } : f.type ), })); return { sampleVariants: sample, totalChildren: cs.children.length }; ``` This gives you positions (grid working?), dimensions (size differentiation?), and fill info (bindings working?) without needing vision. **When to take a screenshot:** - After EVERY completed component (mandatory — part of the user checkpoint) - After creating the foundations documentation page - After final QA (screenshot every page) - Do NOT screenshot after every intermediate step (wastes tool calls) ### Common issues | Symptom | Likely cause | Fix | |---|---|---| | All variants stacked at (0,0) | `combineAsVariants` was called but children were never repositioned | Re-run grid layout script | | Variants show wrong colors | Variable bindings applied after `combineAsVariants` instead of before | Rebind on component set children | | Variant count wrong | Clone loop indexing error | Print `components.map(c => c.name)` before combining | | BOOLEAN property has no effect | `componentPropertyReferences` was set on the component set frame, not on the child node | Find the actual child node and set references there | | INSTANCE_SWAP shows no swap option | Default value was not a valid component ID | Pass a real existing component ID as `defaultValue` | | `combineAsVariants` throws | At least one node in the array is not a `ComponentNode` | Filter array: `nodes.filter(n => n.type === 'COMPONENT')` | | `addComponentProperty` returns unexpected key | Expected — the key gets a `#id:id` suffix | Save the returned value immediately: `const key = cs.addComponentProperty(...)` | --- ## 10. Complete Worked Example: Button Component This shows the full sequence of `use_figma` calls for a Button component, including state passing between calls. Replace `RUN_ID` and variable IDs with your actual values from the state ledger. ### Call 1: Create the component page **Goal:** Create (or find) the Button page. **State input:** None **State output:** `{ pageId }` ```javascript let page = figma.root.children.find(p => p.name === 'Button'); if (!page) { page = figma.createPage(); page.name = 'Button'; } page.setSharedPluginData('dsb', 'run_id', 'ds-build-2024-001'); page.setSharedPluginData('dsb', 'key', 'page/button'); return { pageId: page.id }; ``` ### Call 2: Create documentation frame **Goal:** Add title + description frame. **State input:** `{ pageId }` **State output:** `{ docFrameId }` ```javascript const PAGE_ID = 'PAGE_ID_FROM_STATE'; const page = await figma.getNodeByIdAsync(PAGE_ID); await figma.setCurrentPageAsync(page); // Idempotency check const existing = page.findAll(n => n.getSharedPluginData('dsb', 'key') === 'doc/button'); if (existing.length > 0) { return { docFrameId: existing[0].id }; } await figma.loadFontAsync({ family: 'Inter', style: 'Bold' }); await figma.loadFontAsync({ family: 'Inter', style: 'Regular' }); const docFrame = figma.createFrame(); docFrame.name = 'Button / Documentation'; docFrame.x = 40; docFrame.y = 40; docFrame.layoutMode = 'VERTICAL'; docFrame.primaryAxisSizingMode = 'AUTO'; docFrame.counterAxisSizingMode = 'FIXED'; docFrame.resize(560, 100); docFrame.paddingTop = 40; docFrame.paddingBottom = 40; docFrame.paddingLeft = 40; docFrame.paddingRight = 40; docFrame.itemSpacing = 16; docFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]; const title = figma.createText(); title.fontName = { family: 'Inter', style: 'Bold' }; title.fontSize = 32; title.characters = 'Button'; docFrame.appendChild(title); const desc = figma.createText(); desc.fontName = { family: 'Inter', style: 'Regular' }; desc.fontSize = 14; desc.characters = 'Buttons allow users to take actions with a single tap. Use Primary for the highest-emphasis action on a page, Secondary for supporting actions.'; desc.layoutSizingHorizontal = 'FILL'; docFrame.appendChild(desc); docFrame.setSharedPluginData('dsb', 'run_id', 'ds-build-2024-001'); docFrame.setSharedPluginData('dsb', 'key', 'doc/button'); return { docFrameId: docFrame.id }; ``` ### Call 3: Create base component **Goal:** Create the base component with auto-layout and all variable bindings. **State input:** `{ pageId }` + variable IDs from Phase 1 **State output:** `{ baseCompId }` *(See Section 3 for full code — substituting the actual variable IDs from the state ledger.)* ### Call 4: Create all variants **Goal:** Clone base and produce all 18 variants (3 Size × 2 Style × 3 State). **State input:** `{ pageId, baseCompId }` + variable IDs **State output:** `{ variantIds: ['id1', 'id2', ..., 'id18'] }` ```javascript const RUN_ID = 'ds-build-2024-001'; const BASE_ID = 'BASE_COMP_ID_FROM_STATE'; const PAGE_ID = 'PAGE_ID_FROM_STATE'; // Variable IDs from state ledger: const VAR = { bg_primary: 'VAR_ID_1', text_primary: 'VAR_ID_2', bg_secondary: 'VAR_ID_3', text_secondary: 'VAR_ID_4', bg_disabled: 'VAR_ID_5', text_disabled: 'VAR_ID_6', padding_sm: 'VAR_ID_7', padding_md: 'VAR_ID_8', padding_lg: 'VAR_ID_9', }; const page = await figma.getNodeByIdAsync(PAGE_ID); await figma.setCurrentPageAsync(page); const base = await figma.getNodeByIdAsync(BASE_ID); // Load all variables const vars = {}; for (const [k, v] of Object.entries(VAR)) { vars[k] = await figma.variables.getVariableByIdAsync(v); } const axes = { Size: ['Small', 'Medium', 'Large'], Style: ['Primary', 'Secondary'], State: ['Default', 'Hover', 'Disabled'], }; const paddingMap = { Small: vars.padding_sm, Medium: vars.padding_md, Large: vars.padding_lg }; const components = []; for (const size of axes.Size) { for (const style of axes.Style) { for (const state of axes.State) { const clone = base.clone(); clone.name = `Size=${size}, Style=${style}, State=${state}`; clone.setBoundVariable('paddingTop', paddingMap[size]); clone.setBoundVariable('paddingBottom', paddingMap[size]); clone.setBoundVariable('paddingLeft', paddingMap[size]); clone.setBoundVariable('paddingRight', paddingMap[size]); const isDisabled = state === 'Disabled'; const bgV = isDisabled ? vars.bg_disabled : (style === 'Primary' ? vars.bg_primary : vars.bg_secondary); const txV = isDisabled ? vars.text_disabled : (style === 'Primary' ? vars.text_primary : vars.text_secondary); clone.fills = [figma.variables.setBoundVariableForPaint( { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', bgV )]; const labelNode = clone.findOne(n => n.name === 'label'); labelNode.fills = [figma.variables.setBoundVariableForPaint( { type: 'SOLID', color: { r: 1, g: 1, b: 1 } }, 'color', txV )]; clone.setSharedPluginData('dsb', 'run_id', RUN_ID); clone.setSharedPluginData('dsb', 'key', `component/button/variant/${size}/${style}/${state}`); components.push(clone); } } } return { variantIds: components.map(c => c.id) }; ``` ### Call 5: combineAsVariants + grid layout **Goal:** Combine all 18 variants into a ComponentSet and lay them out in a grid. **State input:** `{ pageId, variantIds }` (18 IDs) **State output:** `{ componentSetId }` *(See Section 5 for full code.)* ### Call 6: Add component properties **Goal:** Add TEXT, BOOLEAN, INSTANCE_SWAP properties and wire them to child nodes. **State input:** `{ pageId, componentSetId }` **State output:** `{ componentSetId, properties: { labelKey, showIconKey, iconKey } }` ```javascript const CS_ID = 'CS_ID_FROM_STATE'; const DEFAULT_ICON_ID = 'ICON_COMP_ID_FROM_STATE'; const page = figma.root.children.find(p => p.name === 'Button'); await figma.setCurrentPageAsync(page); const cs = await figma.getNodeByIdAsync(CS_ID); cs.description = 'Buttons allow users to take actions and make choices with a single tap.'; cs.documentationLinks = [{ uri: 'https://your-storybook.com/button' }]; // Add properties — save returned keys const labelKey = cs.addComponentProperty('Label', 'TEXT', 'Button'); const showIconKey = cs.addComponentProperty('Show Icon', 'BOOLEAN', true); const iconKey = cs.addComponentProperty('Icon', 'INSTANCE_SWAP', DEFAULT_ICON_ID); // Wire to children for (const child of cs.children) { const labelNode = child.findOne(n => n.name === 'label'); if (labelNode) labelNode.componentPropertyReferences = { characters: labelKey }; const iconNode = child.findOne(n => n.name === 'icon'); if (iconNode) { iconNode.componentPropertyReferences = { visible: showIconKey, ...(iconNode.type === 'INSTANCE' ? { mainComponent: iconKey } : {}), }; } } return { componentSetId: cs.id, properties: { labelKey, showIconKey, iconKey }, }; ``` ### Call 7: Validate with get_metadata **Goal:** Structural check — variant count, properties, axes. **Action:** Call `get_metadata` on the ComponentSet node ID (from state). Verify in the result: - `children.length === 18` - `variantGroupProperties` has `Size`, `Style`, `State` keys with correct value arrays - `componentPropertyDefinitions` has `Label`, `Show Icon`, `Icon` entries ### Call 8: Validate with get_screenshot **Goal:** Visual check — layout, colors, text. **Action:** Call `get_screenshot` on the Button page. Inspect the screenshot. If variants are stacked, re-run Call 5. If colors look wrong, inspect variable bindings. ### Checkpoint After Call 8: show the screenshot to the user. Ask: "Here's the Button component with 18 variants. Does this look correct?" Do not proceed to the next component until the user approves.