Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
@@ -0,0 +1,972 @@
> 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.