> 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(
  '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">' +
  '<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
  '</svg>'
);

// 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.
