references/code-connect-setup.md
> Part of the [figma-generate-library skill](../SKILL.md).
# Code Connect Setup Reference
This reference covers all Code Connect tooling available to the figma-generate-library agent: the `add_code_connect_map` tool, `get_code_connect_map` for verification, `send_code_connect_mappings` for bulk application, variable code syntax, framework labels, and the decision of when to map per-component vs. in a final pass.
---
## 1. What Code Connect Does
Code Connect links a Figma component node to its code implementation so that:
- **Dev Mode** shows a real code snippet (from your codebase) instead of an auto-generated approximation when a developer inspects a component.
- **MCP `get_design_context`** returns `componentName`, `source`, and a rendered snippet alongside design tokens, enabling accurate AI-assisted code generation.
- **`search_design_system`** can return code references alongside Figma component metadata.
---
## 2. The Three MCP Tools
### 2a. add_code_connect_map — single mapping
Maps one Figma node to one code component.
**Parameters:**
| Parameter | Type | Required | Notes |
|-----------|------|----------|-------|
| `nodeId` | string | Yes (remote) / Optional (desktop) | Format `123:456`. Must be a published component or component set. |
| `fileKey` | string | Yes (remote) | The Figma file key. |
| `source` | string | Yes | Path in the codebase (e.g. `src/components/Button.tsx`) or a URL. |
| `componentName` | string | Yes | The code component name (e.g. `Button`). |
| `label` | enum | Yes | Framework label — see Section 4 for valid values. |
| `template` | string | Optional | Executable JS template code. Providing this creates a **template** mapping instead of a simple **component-path** mapping. Gated behind a server-side feature flag — falls back to a simple mapping when disabled. |
| `templateDataJson` | string | Optional | JSON string with optional fields: `isParserless`, `imports`, `nestable`, `props`. |
**Two mapping tiers:**
1. **Simple mapping (component-path):** Only `source`, `componentName`, and `label` provided. Associates the Figma component with a code path + name. Dev Mode generates a basic JSX snippet from Figma prop names. This is the default — use it first.
2. **Template mapping:** `template` is also provided. The template is executed in a sandboxed QuickJS environment and dynamically renders the snippet based on the actual instance's property values. Use this when precise prop-level Code Connect is required by the user.
**Common error codes:**
| Error | Meaning | Fix |
|-------|---------|-----|
| `CODE_CONNECT_MAPPING_ALREADY_EXISTS` | Component is already mapped | Disconnect existing mapping in Figma UI first. Do not retry. |
| `CODE_CONNECT_COMPONENT_NOT_PUBLISHED` | The component exists in this file but has never been published | Publish the library first. Do not retry until it is published. |
| `CODE_CONNECT_COMPONENT_UNPUBLISHED` | The component was published, then unpublished | Republish it. Do not retry until it is republished. |
| `CODE_CONNECT_EXTERNAL_LIBRARY_COMPONENT` | The node is a copy of a component from another library | Map it from the library file that defines it |
| `CODE_CONNECT_NOT_A_COMPONENT` | The node is a frame, instance, or other non-component | For an instance, map the main component ID given in the message |
| `CODE_CONNECT_NODE_NOT_FOUND` | No node with this ID in the file | Use a `123:456` node ID from this file |
| `CODE_CONNECT_ASSET_NOT_FOUND` | No published component at this node, reason unknown | Check the node ID and that the component is published |
| `CODE_CONNECT_INSUFFICIENT_PERMISSIONS` | No edit access | Request edit permission on the file |
**Usage example:**
```
Tool: add_code_connect_map
Args: {
nodeId: "123:456",
fileKey: "abc123",
source: "src/components/Button.tsx",
componentName: "Button",
label: "React"
}
```
---
### 2b. get_code_connect_map — verification
Retrieves the current Code Connect mapping for a node. Use this immediately after `add_code_connect_map` to confirm the mapping was saved, and before `send_code_connect_mappings` to audit existing state.
**Parameters:**
| Parameter | Type | Required | Notes |
|-----------|------|----------|-------|
| `nodeId` | string | Optional | The node to check. Omit to get all mappings in the file. |
| `fileKey` | string | Yes (remote) | The Figma file key. |
| `codeConnectLabel` | string | Optional | Filter results to a specific framework label. |
**Returns:** A map of `nodeId -> { componentName, source, label, snippet, snippetImports }`.
**How to verify:**
```
1. Call add_code_connect_map with the node.
2. Immediately call get_code_connect_map(nodeId, fileKey).
3. Confirm the returned object has the expected componentName and source.
4. If the mapping is missing, check for error codes from step 1.
```
---
### 2c. send_code_connect_mappings — bulk application
Applies multiple Code Connect mappings in one call. Use after `get_code_connect_suggestions` returns a batch of unmapped components, or when doing a final-pass bulk mapping at the end of Phase 4.
**Parameters:**
| Parameter | Type | Required | Notes |
|-----------|------|----------|-------|
| `nodeId` | string | Optional | Context node for design fallback if mappings array is empty. |
| `fileKey` | string | Yes (remote) | The Figma file key. |
| `mappings` | array | Yes | Array of mapping objects. |
**Each mapping object:**
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `nodeId` | string | Yes | The Figma node identifier. |
| `componentName` | string | Yes | Code component name. |
| `source` | string | Yes | Path in the codebase. |
| `label` | enum | Yes | Framework label. |
| `template` | string | Optional | JS template code for template mapping. |
| `templateDataJson` | string | Optional | JSON template metadata. |
**Behavior:**
- All mappings are processed in parallel via POSTs to the backend.
- If any mapping fails, errors are reported per mapping — the rest succeed.
- On full success, `get_design_context` is called for the nodes and fresh design context is returned.
**Bulk workflow:**
```
1. Collect all {nodeId, componentName, source, label} pairs.
2. Call send_code_connect_mappings({ fileKey, mappings: [...all pairs...] }).
3. Review reported errors. Fix the cause the message names before retrying; never retry a mapping that failed with `CODE_CONNECT_MAPPING_ALREADY_EXISTS`, `CODE_CONNECT_COMPONENT_NOT_PUBLISHED`, or `CODE_CONNECT_COMPONENT_UNPUBLISHED` unchanged.
4. Call get_code_connect_map on a sample of nodes to spot-check.
```
---
## 3. Variable Code Syntax (Token Round-Tripping)
Setting code syntax on variables creates the bidirectional link between Figma tokens and the codebase token system. This is what enables Dev Mode to show `var(--color-bg-primary)` next to a design value instead of a raw hex.
**The three platforms:**
```javascript
// In use_figma:
variable.setVariableCodeSyntax('WEB', 'var(--color-bg-primary)');
variable.setVariableCodeSyntax('ANDROID', 'Theme.colorBgPrimary');
variable.setVariableCodeSyntax('iOS', 'Color.bgPrimary');
```
- `WEB` — used for CSS custom properties, design token JSON, and any web framework.
- `ANDROID` — used for Jetpack Compose theme references and Android resource names.
- `iOS` — used for SwiftUI Color extensions and UIKit color methods.
**Derivation rules (in priority order):**
1. **Best:** Use the exact token name from the codebase. Search the codebase for CSS custom properties (`--`), Swift color extensions, or Kotlin theme references and use those exact strings.
2. **Good:** Derive from the Figma variable name with a consistent transformation: replace `/` and spaces with `-`, prefix with `var(--` and suffix with `)`.
- Example: `color/bg/primary` → `var(--color-bg-primary)`
3. **Avoid:** Guessing or inventing names that don't exist in the codebase.
**Consistency rule:** The transformation must be uniform. If you use `var(--color-bg-primary)` for one variable, use the same `var(--{path-with-hyphens})` pattern for all variables in that collection.
**WEB syntax bulk example:**
```javascript
// In use_figma — set WEB code syntax on every variable in matching collections.
// flatMap all variable IDs across matching collections into a single
// Promise.all so the lookups run in parallel across collections too — same
// pattern as `listVariableCollectionsAndVariables` in variable-patterns.md.
// setVariableCodeSyntax is sync, so the writes don't need to be batched.
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const ids = collections
.filter(c => c.name === 'Color')
.flatMap(c => c.variableIds);
const vars = await Promise.all(
ids.map(id => figma.variables.getVariableByIdAsync(id))
);
for (const v of vars) {
if (!v) continue;
// Derive: "color/bg/primary" → "var(--color-bg-primary)"
const cssName = 'var(--' + v.name.toLowerCase().replace(/\//g, '-').replace(/\s+/g, '-') + ')';
v.setVariableCodeSyntax('WEB', cssName);
}
```
---
## 4. Framework Labels
The following labels are valid for all Code Connect MCP operations. Use the label that matches your codebase framework.
| Label | Use for |
|-------|---------|
| `React` | React / JSX / TSX components |
| `Web Components` | Native Web Components, Lit, FAST |
| `Vue` | Vue 2 and Vue 3 SFCs |
| `Svelte` | Svelte components |
| `Storybook` | Storybook stories with Code Connect integration |
| `Javascript` | Plain JavaScript, framework-agnostic |
| `Swift` | Swift / UIKit |
| `Swift UIKit` | UIKit specifically |
| `Objective-C UIKit` | Objective-C with UIKit |
| `SwiftUI` | SwiftUI view components |
| `Compose` | Jetpack Compose (Android) |
| `Java` | Java Android components |
| `Kotlin` | Kotlin Android (non-Compose) |
| `Android XML Layout` | Android XML layout files |
| `Flutter` | Flutter / Dart widgets |
| `Markdown` | Documentation or MDX components |
**HTML note:** The label `HTML` is used by the Code Connect CLI's HTML parser (for Angular, Vue, and Web Components without a framework-specific parser), but the MCP tools use `Web Components` or `Vue` directly. Check the codebase framework before selecting.
---
## 5. Per-Component vs. Final-Pass Strategy
### Per-component (preferred for new builds)
Map Code Connect immediately after creating a component, while the context is fresh (Phase 3, step 3h in the SKILL.md workflow):
**Advantages:**
- The node ID is already in hand from the creation script.
- You know exactly which code component this Figma component corresponds to (you just designed it to match).
- Errors surface early, before building dependent components.
**When to use:** Any time you create a Figma component that has a clear 1:1 match with an existing code component.
### Final pass (for bulk mapping at Phase 4)
Collect all unmapped components and map them in one `send_code_connect_mappings` call:
**Advantages:**
- One bulk call instead of N individual calls.
- Can use `get_code_connect_suggestions` to discover unmapped components automatically.
- Better for importing existing Figma files where you didn't control creation.
**When to use:** Retrofitting Code Connect onto an existing file, or when the codebase mapping requires research that is better done after all components are created.
### Hybrid (recommended for large systems)
- Map atoms (Button, Input, Badge, Avatar) **per-component** during Phase 3.
- Map molecules and organisms in a **final pass** during Phase 4 after all atoms are mapped, since molecule snippets reference atom Code Connect IDs.
---
## 6. Verification in Dev Mode
After mapping:
1. Open the Figma file in the browser or desktop app.
2. Switch to Dev Mode (the `</>` icon in the toolbar).
3. Select a component instance (not the main component — an instance placed on a page).
4. In the Inspect panel, the code snippet should show the Code Connect output instead of auto-generated code.
5. If the snippet is missing or shows `[auto-generated]`, run `get_code_connect_map` via MCP to confirm the mapping exists, then check that the component is published.
**Via MCP (faster during agent workflows):**
```
get_code_connect_map(nodeId: "<the component set node ID>", fileKey: "<file key>")
```
The response should include `componentName`, `source`, `label`, and a non-empty `snippet`.
---
## 7. Important Constraints
- **Published components only:** `add_code_connect_map` requires the component to be published to a library. Components created in this session are not published yet, so mapping them fails with `CODE_CONNECT_COMPONENT_NOT_PUBLISHED` until the library is published.
- **One mapping per label per node:** A node can have multiple mappings (one per framework label), but only one per label. Attempting to add a second React mapping to the same node returns `CODE_CONNECT_MAPPING_ALREADY_EXISTS`.
- **Template mappings are gated:** The `template` parameter is gated behind a server-side feature flag and may not be available in every environment. Use simple mappings unless the user explicitly requests template-level Code Connect.
- **Start simple, escalate:** Always begin with simple mappings (`source` + `componentName` + `label`). Add `template` only if the user needs precise prop-level snippet rendering.
references/component-creation.md
> Part of the [figma-generate-library skill](../SKILL.md).
# Component Creation Reference
Guide to building components with variant matrices, variable bindings, component properties, and optional documentation. The scope selected in the main skill determines which sections apply.
> **Design files only.** Every snippet here (including `figma.createPage()`) targets Figma Design files (`figma.com/design/...`). `figma.createPage()` throws in both FigJam (`figma.com/board/...`) and Slides (`figma.com/slides/...`).
>
> **Every text mutation in this file follows the [canonical text-edit recipe](../../figma-use/references/gotchas.md#canonical-text-edit-recipe-font-load--await--mutate--return-ids):** load font → `await` → mutate → return affected IDs. Examples use `Inter` because it's available everywhere; `loadFontAsync` is required for every (family, style) pair you mutate, including non-Inter brand fonts.
---
## 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, create that dependency first. Build the relevant dependency graph during discovery 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
For a full library, each component usually lives on a dedicated page with documentation and its component set. For a scoped component task, match the file's existing organization and create a page or documentation only when the selected scope requires it.
```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);
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 page = await figma.getNodeByIdAsync('PAGE_ID_FROM_STATE');
if (!page || page.type !== 'PAGE') throw new Error('Expected the selected component page');
await figma.setCurrentPageAsync(page);
// 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';
// 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);
label.layoutSizingHorizontal = 'HUG';
label.layoutSizingVertical = 'HUG';
// --- 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);
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 BASE_COMP_ID = 'BASE_ID_FROM_STATE'; // from state ledger
const page = await figma.getNodeByIdAsync('PAGE_ID_FROM_STATE');
if (!page || page.type !== 'PAGE') throw new Error('Expected the selected component page');
await figma.setCurrentPageAsync(page);
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];
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;
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' && 'characters' in n,
);
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.findAllWithCriteria({ types: ['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];
}
});
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
const propertyOwner = cs?.type === 'COMPONENT' && cs.parent?.type === 'COMPONENT_SET' ? cs.parent : cs;
if (!propertyOwner || !['COMPONENT', 'COMPONENT_SET'].includes(propertyOwner.type)) throw new Error('Expected a component set or non-variant component');
const props = propertyOwner.componentPropertyDefinitions;
const iconPropKey = Object.keys(props).find(k => k.startsWith('Icon'));
// Set preferred values (array of component keys or instance IDs)
propertyOwner.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. 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'`
---
## 8. 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 `COMPONENT_SET` node ID (never an individual variant component ID) 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. When documentation is in scope, capture the component page or documentation container. Otherwise capture the component set or its containing frame so validation does not require a new page.
```
Tool: get_screenshot
Args: { nodeId: "COMPONENT_SET_OR_CONTAINER_ID", fileKey: "FILE_KEY" }
```
**How to use the screenshot:**
1. **Display it to the user** as validation evidence for the completed component.
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 via `get_metadata` and describe what you created.
**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, if included** | Title + description text visible above or beside the component grid | Included documentation is missing or 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 | Call `listAvailableFontsAsync()` to verify the font exists, then check `loadFontAsync` was called before text writes; 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 |
| Included 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 each completed component or coherent component family in the selected scope
- After creating a foundations documentation page, when included
- During final QA for a full library
- 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(...)` |
---
## 9. Complete Worked Example: Button Component
This shows the full-library presentation sequence, including state passing between calls. For a scoped component in an existing component area, skip Calls 1–2, use that area's page ID, and begin at Call 3. Replace variable IDs with actual values from the state ledger.
### Call 1: Create the component page (full library or requested showcase only)
**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'; }
return { pageId: page.id };
```
### Call 2: Create documentation frame (when selected scope includes it)
**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);
const existing = page
.findAllWithCriteria({ types: ['FRAME'] })
.filter(n => n.name === 'Button / Documentation');
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.';
docFrame.appendChild(desc);
desc.layoutSizingHorizontal = 'FILL';
return { docFrameId: docFrame.id };
```
### Call 3: Create base component
**Goal:** Create the base component with auto-layout and all variable bindings.
**State input:** `{ pageId }` + verified or newly created foundation variable IDs
**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 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 in parallel — sequential awaits in the loop would
// serialize one IPC round-trip per variable.
const varEntries = Object.entries(VAR);
const fetched = await Promise.all(
varEntries.map(([, id]) => figma.variables.getVariableByIdAsync(id))
);
const vars = {};
varEntries.forEach(([k], i) => { vars[k] = fetched[i]; });
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
)];
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_ID = 'PAGE_ID_FROM_STATE';
const page = await figma.getNodeByIdAsync(PAGE_ID);
if (!page || page.type !== 'PAGE') throw new Error('Expected the selected component page');
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' && 'characters' in n,
);
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 `COMPONENT_SET` node ID from state, not on any child variant component. 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 component set or containing frame; use the Button page when documentation is in scope. Inspect the screenshot. If variants are stacked, re-run Call 5. If colors look wrong, inspect variable bindings.
### Completion check
After Call 8, inspect and show the screenshot as validation evidence. Repair defects in the selected scope; when its definition of done passes, stop or continue to the next in-scope component.
references/discovery-phase.md
> Part of the [figma-generate-library skill](../SKILL.md).
# Discovery Phase Reference
This document covers everything needed for Phase 0 of a design system build: analyzing the codebase for tokens, inspecting the Figma file for existing conventions, searching subscribed libraries, building the plan, and resolving conflicts before any write operations begin.
---
## 1. Codebase Analysis — Finding Token Sources
### Search Priority Order
Look for token sources in this order. Stop as soon as you find a definitive source; multiple formats can coexist:
1. Design token files: `*.tokens.json`, `tokens/*.json`, `src/tokens/**`
2. CSS variable files: `variables.css`, `tokens.css`, `theme.css`, `global.css`
3. Tailwind config: `tailwind.config.js`, `tailwind.config.ts`
4. CSS-in-JS theme objects: `theme.ts`, `createTheme`, `ThemeProvider`
5. Platform-specific: iOS Asset catalogs (`.xcassets`), Android `themes.xml`, `colors.xml`
### CSS Custom Properties (Most Common for Web)
**What to search for:**
```
:root { ... }
@theme { ... } ← Tailwind v4
--color-*, --spacing-*, --radius-*, --shadow-*, --font-*
```
**Pattern:** `/--[\w-]+:\s*[^;]+/g`
**Common file locations:** `src/styles/tokens.css`, `src/styles/variables.css`, `src/theme/*.css`
**Extraction and naming translation:**
| CSS Property | Figma Variable Name | Figma Type | WEB Code Syntax |
|---|---|---|---|
| `--color-bg-primary: #fff` | `color/bg/primary` | COLOR | `var(--color-bg-primary)` |
| `--color-text-secondary: #757575` | `color/text/secondary` | COLOR | `var(--color-text-secondary)` |
| `--spacing-sm: 8px` | `spacing/sm` | FLOAT | `var(--spacing-sm)` |
| `--radius-md: 8px` | `radius/md` | FLOAT | `var(--radius-md)` |
| `--font-body: "Inter"` | `typography/body/font-family` | STRING | `var(--font-body)` |
**Naming rule:** Replace hyphens with slashes at category boundaries. Keep hyphens within the final path segment: `--color-bg-primary` → `color/bg/primary`, but `--color-bg-primary-hover` → `color/bg/primary-hover`.
**Always store the original CSS variable name** as the code syntax value — never derive it from the Figma variable name. If the codebase uses `--sds-color-background-brand-default`, use exactly that string in `setVariableCodeSyntax('WEB', '--sds-color-background-brand-default')`.
### Tailwind Configuration
**What to look for in `tailwind.config.js` or `tailwind.config.ts`:**
```javascript
// theme.extend.colors → Figma color variables
{ primary: { DEFAULT: '#3366FF', light: '#6699FF', dark: '#0033CC' } }
// → color/primary/default, color/primary/light, color/primary/dark
// theme.extend.spacing → Figma FLOAT variables
{ 'xs': '4px', 'sm': '8px', 'md': '16px' }
// → spacing/xs = 4, spacing/sm = 8, spacing/md = 16
// theme.extend.borderRadius → Figma FLOAT variables
{ 'sm': '4px', 'md': '8px', 'lg': '16px' }
// → radius/sm = 4, radius/md = 8, radius/lg = 16
```
Tailwind utility class names (`bg-blue-500`, `p-4`) are not tokens — extract values from the config object, not the class names.
### Design Token Community Group (DTCG) Format
**Pattern:** `*.tokens.json` or `tokens/*.json`. Find source files, not generated outputs from Style Dictionary or Tokens Studio.
```json
{
"color": {
"bg": {
"primary": { "$type": "color", "$value": "#ffffff" },
"secondary": { "$type": "color", "$value": "#f5f5f5" }
}
},
"spacing": {
"sm": { "$type": "dimension", "$value": "8px" }
}
}
```
Nested keys map to slash-separated Figma names: `color.bg.primary` → `color/bg/primary`.
### CSS-in-JS / Theme Objects
**What to search for:** `createTheme`, `ThemeProvider`, `theme = {}`, styled-components, Emotion, Stitches, vanilla-extract
```typescript
// theme.colors.bg.primary → Figma variable: color/bg/primary
// theme.spacing.sm → Figma variable: spacing/sm
// Multiple theme objects (lightTheme, darkTheme) → modes in the same collection
```
### iOS Token Sources
```swift
// Asset catalog colors in .xcassets/Colors.xcassets
// extension Color { static let bgPrimary = Color("bg-primary") }
// Look for traitCollection.userInterfaceStyle for dark mode detection
```
### Android Token Sources
```kotlin
// res/values/colors.xml <color name="primary">#3366FF</color>
// res/values-night/colors.xml (dark mode overrides)
// MaterialTheme.colorScheme.primary in Compose
// val Primary = Color(0xFF3366FF)
```
### Detecting Dark Mode
| Platform | Signal |
|---|---|
| Web (CSS) | `@media (prefers-color-scheme: dark)`, `.dark { }`, `[data-theme="dark"]` |
| Web (Tailwind) | `darkMode: 'class'` or `darkMode: 'media'` in config |
| Web (JS) | Separate `darkTheme` object alongside `lightTheme` |
| iOS | `Color(uiColor:)` with `traitCollection.userInterfaceStyle`, dual-appearance asset catalog |
| Android | `themes.xml` with `Theme.*.Night`, `isSystemInDarkTheme()` in Compose, `values-night/` folder |
**Figma mapping:** If dark mode exists → minimum 2 modes (Light/Dark) in the semantic color collection. Primitive collections stay single-mode.
### Shadow/Elevation Extraction
Shadows cannot be Figma variables — they become **Effect Styles**.
```css
/* Look for: box-shadow, --shadow-* */
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.10);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.10);
```
CSS `0 4px 6px -1px rgba(0,0,0,0.1)` → Figma:
```
{ type: "DROP_SHADOW", offset: {x:0, y:4}, radius: 6, spread: -1, color: {r:0, g:0, b:0, a:0.1} }
```
### Typography Extraction
| Code token | Maps to |
|---|---|
| `font-size: 16px` | FLOAT variable (scope `FONT_SIZE`) or Text Style `fontSize` |
| `line-height: 1.5` | Text Style `lineHeight: {value: 24, unit: "PIXELS"}` |
| `font-weight: 600` | STRING variable (scope `FONT_STYLE`, holds a font-specific style name like `"Regular"` — discover via `listAvailableFontsAsync()`) or Text Style `fontName.style` |
| `letter-spacing: -0.02em` | Text Style `letterSpacing: {value: -2, unit: "PERCENT"}` |
| `font-family: "Inter"` | STRING variable (scope `FONT_FAMILY`) or Text Style `fontName.family` |
Composite text styles (all properties bundled) → Figma Text Styles. Individual properties → Figma variables with appropriate scopes.
### Component Extraction
For each component, extract:
1. **Name** → Figma component set name
2. **Union-type props** → VARIANT properties
3. **String content props** → TEXT properties
4. **Boolean props** → BOOLEAN properties (and VARIANT State when combined with interaction states)
5. **Child/slot props** → INSTANCE_SWAP properties
```typescript
// React example:
interface ButtonProps {
size: 'sm' | 'md' | 'lg'; // → VARIANT: Size = sm|md|lg
variant: 'primary' | 'secondary'; // → VARIANT: Style = primary|secondary
disabled?: boolean; // → VARIANT: State (combine: default|hover|pressed|disabled)
label: string; // → TEXT: Label
icon?: ReactNode; // → INSTANCE_SWAP: Icon + BOOLEAN: Show Icon
}
// → Component Set "Button", variant count: 3 sizes × 2 styles × 4 states = 24
```
---
## 2. Figma File Inspection
Run these `use_figma` snippets during scoped discovery. All are read-only and safe to run before any mutation.
### List All Pages
```javascript
const pages = figma.root.children.map((p, i) => ({
index: i,
name: p.name,
id: p.id,
childCount: p.children.length
}));
return { pages };
```
Interpret: note page names for naming convention (are they PascalCase? sentence case?), count separator pages (`---`), identify existing component pages vs foundations pages.
### List Variable Collections With Modes
```javascript
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const result = collections.map(c => ({
id: c.id,
name: c.name,
modes: c.modes, // [{modeId, name}, ...]
variableCount: c.variableIds.length,
defaultModeId: c.defaultModeId
}));
return { collections: result };
```
Interpret: identify existing primitive/semantic split, note mode names (do they use "Light/Dark" or "SDS Light/SDS Dark"?), count variables to understand scope.
### List Variables in a Collection (with names, types, scopes, and sample values)
```javascript
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const targetName = "Color"; // change to the collection you want to inspect
const coll = collections.find(c => c.name === targetName);
if (!coll) { return { error: `Collection "${targetName}" not found` }; }
const allVars = await figma.variables.getLocalVariablesAsync();
const vars = allVars.filter(v => v.variableCollectionId === coll.id);
const result = vars.map(v => ({
id: v.id,
name: v.name,
resolvedType: v.resolvedType,
scopes: v.scopes,
codeSyntax: v.codeSyntax,
// First mode value only, for a sample
sampleValue: v.valuesByMode[coll.defaultModeId]
}));
return { collection: coll.name, variableCount: result.length, variables: result };
```
Interpret: check if variables use `ALL_SCOPES` (bad), check naming convention (slash-separated hierarchy?), check if code syntax is set, identify alias chains.
### List Component Sets with Properties
```javascript
// To inspect a specific page, switch to it first:
// await figma.setCurrentPageAsync(targetPage);
const componentSets = figma.currentPage.findAllWithCriteria({ types: ['COMPONENT_SET'] });
const result = componentSets.map(cs => ({
id: cs.id,
name: cs.name,
variantCount: cs.children.length,
properties: Object.entries(cs.componentPropertyDefinitions).map(([key, def]) => ({
name: key,
type: def.type,
variantOptions: def.variantOptions || null,
defaultValue: def.defaultValue
}))
}));
return { componentSets: result, count: result.length };
```
Note: to search ALL pages, **do not iterate `figma.root.children` and `setCurrentPageAsync` inside one script.** Run a cheap discovery call first (`figma.root.children.map(p => ({id: p.id, name: p.name}))`), then in the next assistant turn emit **one `use_figma` per page in parallel** — a single message with N tool-use blocks — each setting `currentPage` once. See [figma-use → gotchas.md → Set current page once per `use_figma` call](../../figma-use/references/gotchas.md#set-current-page-once-per-use_figma-call--split-multi-page-work-into-parallel-calls).
### List All Styles
```javascript
const [textStyles, effectStyles, paintStyles] = await Promise.all([
figma.getLocalTextStylesAsync(),
figma.getLocalEffectStylesAsync(),
figma.getLocalPaintStylesAsync()
]);
return {
textStyles: textStyles.map(s => ({ id: s.id, name: s.name, fontSize: s.fontSize, fontName: s.fontName })),
effectStyles: effectStyles.map(s => ({ id: s.id, name: s.name, effectCount: s.effects.length })),
paintStyles: paintStyles.map(s => ({ id: s.id, name: s.name })),
counts: { text: textStyles.length, effect: effectStyles.length, paint: paintStyles.length }
};
```
### Check Naming Conventions on an Existing Component
```javascript
// Replace with the node ID of an existing component to analyze
const node = await figma.getNodeByIdAsync("YOUR_NODE_ID");
if (!node) { return { error: "Node not found" }; }
// Check fills for variable bindings
const fillInfo = [];
if ('fills' in node && Array.isArray(node.fills)) {
for (const fill of node.fills) {
if (fill.type === 'SOLID' && fill.boundVariables?.color) {
fillInfo.push({ type: 'variable_alias', id: fill.boundVariables.color.id });
} else if (fill.type === 'SOLID') {
fillInfo.push({ type: 'hardcoded', r: fill.color.r, g: fill.color.g, b: fill.color.b });
}
}
}
return {
name: node.name,
type: node.type,
fills: fillInfo
};
```
---
## 3. Library Discovery and search_design_system
### Step 1: Discover available libraries with `get_libraries`
Before searching, call `get_libraries` to see what libraries the file has access to:
```
get_libraries({ fileKey: "abc123" })
// offset is optional; omit (or pass 0) for the first page
```
Returns:
- **`libraries_added_to_file`** — libraries currently subscribed (team libraries, community UI kits already enabled)
- **`libraries_available_to_add`** — community UI kits and org libraries not yet subscribed
- **`libraries_available_to_add_next_offset`** — non-null when more org libraries are available; pass it back as `offset` to fetch the next page
Each library entry includes `name`, `libraryKey`, `description`, and `source` ("team", "community", or "organization"). Use the `libraryKey` values to scope searches in the next step.
**Pagination.** Org libraries paginate in batches of 20. Community UI kits are only returned on the first page (`offset=0`), so subsequent pages contain only org libraries. If the user is looking for a specific library by name and it isn't in the current page, page further (call `get_libraries` again with `offset: libraries_available_to_add_next_offset`) or ask them to subscribe it to the file.
```
// Page 1
get_libraries({ fileKey: "abc123" })
// → { ..., libraries_available_to_add_next_offset: 20 }
// Page 2
get_libraries({ fileKey: "abc123", offset: 20 })
// → { ..., libraries_available_to_add_next_offset: 40 | null }
```
### Step 2: Search with `search_design_system`
`search_design_system` searches design libraries for the given file. Each `queries` entry names one entity type:
1. **Components** — published library components, searched by name/description via a recommendation engine (relevance-ranked, not exact match)
2. **Variables** — design tokens (colors, spacing, etc.) across subscribed libraries
3. **Styles** — paint styles, text styles, and effect styles
By default it searches all accessible libraries. Pass `includeLibraryKeys` to search within specific libraries only. This is useful when you have many libraries and want targeted results.
### Input
```
// Search all libraries
search_design_system({
queries: [
{ entity: "component", query: "button" },
{ entity: "variable", query: "color" },
{ entity: "style", query: "heading" }
],
fileKey: "abc123" // required — your file key
})
// Search a specific library only (use libraryKey from get_libraries)
search_design_system({
queries: [{ entity: "component", query: "button" }],
fileKey: "abc123",
includeLibraryKeys: ["lk-abc123..."]
})
```
### What It Returns
```json
{
"results": [
{
"entity": "component",
"query": "button",
"components": [
{
"name": "Button",
"libraryName": "Design System",
"assetType": "component_set",
"componentSetKey": "abc123def",
"description": "Primary action button"
}
]
},
{
"entity": "variable",
"query": "color",
"variables": [
{
"name": "colors/primary/500",
"variableType": "COLOR",
"variableSetKey": "set1key",
"key": "var1key",
"scopes": ["FRAME_FILL", "SHAPE_FILL"],
"variableCollectionName": "Colors"
}
]
},
{
"entity": "style",
"query": "heading",
"styles": [
{
"name": "Heading/H1",
"styleType": "TEXT",
"key": "style1key"
}
]
}
]
}
```
### How to Interpret Results
**Components:** Inspect `assetType`: use `componentKey` for a `component` and `componentSetKey` for a `component_set`. The example above is a set:
```javascript
const componentSet = await figma.importComponentSetByKeyAsync("abc123def");
```
**Variables:** The `variableSetKey` is the collection key. The `key` is the variable key. Use these to understand what naming conventions are in use, and what tokens are available to alias from.
**Styles:** The `key` is usable with `figma.importStyleByKeyAsync(key)` to import into the current file.
### When to Search
- **During scoped discovery:** Search broadly for the selected inventory (`queries: [{ entity: "component", query: "button" }, { entity: "variable", query: "color" }, { entity: "variable", query: "spacing" }]`). This establishes the reuse baseline.
- **Search again only when needed:** Repeat a component search when it was outside the selected inventory, available libraries changed, or the earlier result did not resolve it. Reuse current results for in-scope components.
**Reuse decision:**
| Condition | Decision |
|---|---|
| Found component with matching variant API, same token model | Import and reuse |
| Found component but wrong variant properties or hardcoded values | Rebuild |
| Found component that matches visually but API is incompatible | Wrap: nest as instance inside a new wrapper component |
---
## 4. Building the Plan
After codebase analysis and Figma inspection, produce a mapping table and present it to the user.
### Token → Variable Mapping Table
For each token found in code, record:
| Code Token | CSS Name | Raw Value | Figma Collection | Figma Variable Name | Figma Type | Mode(s) |
|---|---|---|---|---|---|---|
| `theme.colors.blue[500]` | `--color-blue-500` | `#3B82F6` | Primitives | `blue/500` | COLOR | Value |
| `theme.colors.bg.primary` | `--color-bg-primary` | (light: blue/50, dark: gray/900) | Color | `color/bg/primary` | COLOR | Light, Dark |
| `theme.spacing.sm` | `--spacing-sm` | `8px` | Spacing | `spacing/sm` | FLOAT | Value |
| `theme.radii.md` | `--radius-md` | `8px` | Spacing | `radius/md` | FLOAT | Value |
| `theme.shadows.md` | `--shadow-md` | `0 4px 6px rgba(0,0,0,0.1)` | — | — | Effect Style | — |
### Component → Component Set Mapping Table
| Code Component | Props → Variant Axes | Variant Count | Figma Page | Reuse? |
|---|---|---|---|---|
| `Button` | size (sm/md/lg) × variant (primary/secondary) × state (default/hover/disabled) | 18 | Buttons | Search first |
| `Avatar` | size (sm/md/lg) × type (image/initials/icon) | 9 | Avatars | Search first |
### Gap Identification
Compare what was found in code vs what already exists in Figma:
- **New:** tokens or components that exist in code but not in Figma → create
- **Existing:** tokens or components already in Figma with matching names → verify scope/code-syntax, skip or update
- **Conflict:** same name, different value → escalate to user (see section 5)
- **Figma-only:** exists in Figma but not in code → flag for user, likely skip
### User-Facing Discovery Summary Template
Present one concise scope and acceptance summary, then continue into the selected workflow path. Ask the user only when an unresolved conflict requires a decision.
```
Here's what I found and what I plan to build:
CODEBASE ANALYSIS
Colors: {N} primitives ({families}), {M} semantic tokens ({light/dark if applicable})
Spacing: {N} tokens ({range})
Typography: {N} text styles, {M} individual scale tokens
Shadows: {N} levels → will become Effect Styles
Components: {list of component names}
EXISTING FIGMA FILE
Collections: {N} existing collections
Variables: {M} existing variables
Styles: {K} text, {L} effect, {J} paint styles
Components: {list}
PLAN
New collections: {list with mode counts}
New variables: ~{N} ({breakdown by collection})
New styles: {N} text, {M} effect
New components: {list}
Reusable library results for the selected inventory: {list}
GAPS / CONFLICTS NEEDING DECISIONS
⚠ {conflict description} — Code says X, Figma already has Y. Which wins?
WHAT I WON'T BUILD (and why)
- {item}: already exists in Figma with matching conventions
- {item}: not supported as a Figma variable (e.g. z-index)
```
---
## 5. Conflict Resolution — When Code and Figma Disagree
When the same token/component exists in both code and Figma but with different values, names, or structures, **always ask the user**. Never silently pick one.
### Decision Framework
| Scenario | Ask the user |
|---|---|
| Same CSS name, different hex value (e.g., `--color-accent` is `#3366FF` in code but `#5B7FFF` in Figma) | "Code says `#3366FF`, Figma currently has `#5B7FFF` for `color/accent/default`. Which is correct?" |
| Same component name, different variant axes (code has `size: sm/md/lg`, Figma has `Size: Small/Large`) | "Code uses 3 sizes (sm/md/lg) but Figma has 2 (Small/Large). Should I add Medium, or rename to match code?" |
| Code has a semantic token with no primitive layer; Figma already has a fully-layered system | "The codebase uses a flat single-layer token model. The Figma file uses a primitive/semantic split. Should I match the Figma architecture or the code architecture?" |
| Figma variable exists but has `ALL_SCOPES` (incorrect per best practice) | "I found `color/bg/primary` already exists but it uses ALL_SCOPES. I recommend changing it to `FRAME_FILL, SHAPE_FILL`. May I update the scope?" |
| Code uses camelCase (`backgroundColor`), Figma uses slash-separated (`color/bg/default`) | "The codebase uses camelCase naming. The Figma file uses slash-separated hierarchy. For new variables, should I use slash-separated (Figma standard) and map via code syntax?" |
### Code Wins
Default to code as the source of truth for:
- Hex values (code is the live production value)
- Token naming (the CSS variable names become code syntax)
- Mode values (light/dark split comes from code)
### Figma Wins
Default to Figma as the source of truth for:
- Collection architecture (if a well-structured system already exists, extend it rather than replace it)
- Variable naming hierarchy (if designers are already using the system with specific names)
- Page structure (match the existing page organization pattern)
### Neither: Negotiate
When neither is clearly correct, propose a resolution and ask:
> "I'd suggest [option]. This way both the code token name and the Figma naming convention are preserved. Does that work?"
references/documentation-creation.md
> Part of the [figma-generate-library skill](../SKILL.md).
# Documentation Creation Reference
This reference covers Phase 2 of the design system build: the cover page, foundations documentation page (color swatches, type specimens, spacing bars, shadow cards, radius demo), page layout dimensions, and inline component documentation. Every code block is complete `use_figma`-ready JavaScript (helper-function form — meant to be embedded in a larger script that uses `return` to send results back).
> **Every text mutation in this file follows the [canonical text-edit recipe](../../figma-use/references/gotchas.md#canonical-text-edit-recipe-font-load--await--mutate--return-ids):** load font → `await` → mutate → return affected IDs. Examples use `Inter` because it's available everywhere, but `loadFontAsync` is required for every (family, style) pair you mutate — not just Inter.
> **Design files only.** Every snippet here (including `figma.createPage()`) targets Figma Design files (`figma.com/design/...`). `figma.createPage()` throws in both FigJam (`figma.com/board/...`) and Slides (`figma.com/slides/...`).
---
## 1. Cover Page
The cover page is always the first page in the file. It is a branded title card that sets context for anyone opening the file.
### What to include
- File/system name as a large heading (48–72px)
- Version string or date
- Brief tagline (1 sentence)
- Optional: color block background using the primary brand color variable
### Cover page dimensions
The cover frame should be **1440 × 900px** — this matches the default Figma canvas and looks correct in the page thumbnail.
### use_figma for cover page
```javascript
async function createCoverPage(systemName, tagline, version, primaryColorVar) {
// primaryColorVar: a Figma Variable object for the brand primary fill
const page = figma.createPage();
page.name = 'Cover';
await figma.setCurrentPageAsync(page);
// Batch the font loads — sequential awaits would serialize three IPC
// round-trips that can run in parallel.
await Promise.all([
figma.loadFontAsync({ family: 'Inter', style: 'Bold' }),
figma.loadFontAsync({ family: 'Inter', style: 'Regular' }),
figma.loadFontAsync({ family: 'Inter', style: 'Medium' }),
]);
const frame = figma.createAutoLayout('VERTICAL');
frame.name = 'Cover';
frame.resize(1440, 900);
frame.layoutSizingHorizontal = 'FIXED';
frame.layoutSizingVertical = 'FIXED';
frame.x = 0;
frame.y = 0;
frame.primaryAxisAlignItems = 'CENTER';
frame.counterAxisAlignItems = 'CENTER';
frame.itemSpacing = 16;
// Background: bind to primary variable if provided, else solid dark
if (primaryColorVar) {
const bgPaint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0.05, g: 0.05, b: 0.05 } },
'color',
primaryColorVar
);
frame.fills = [bgPaint];
} else {
frame.fills = [{ type: 'SOLID', color: { r: 0.06, g: 0.06, b: 0.07 } }];
}
page.appendChild(frame);
// System name heading
const title = figma.createText();
title.fontName = { family: 'Inter', style: 'Bold' };
title.characters = systemName;
title.fontSize = 64;
title.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
title.textAlignHorizontal = 'CENTER';
frame.appendChild(title);
// Tagline
const tag = figma.createText();
tag.fontName = { family: 'Inter', style: 'Regular' };
tag.characters = tagline;
tag.fontSize = 20;
tag.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 0.7 } }];
tag.textAlignHorizontal = 'CENTER';
frame.appendChild(tag);
// Version
const ver = figma.createText();
ver.fontName = { family: 'Inter', style: 'Medium' };
ver.characters = version;
ver.fontSize = 13;
ver.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 0.45 } }];
ver.textAlignHorizontal = 'CENTER';
frame.appendChild(ver);
return { page, frameId: frame.id };
}
```
---
## 2. Foundations Page
The Foundations page is always placed **before any component pages**. It visually documents the design tokens — colors, typography, spacing, shadows, and border radii — so designers and engineers can see available primitives at a glance.
### Page layout dimensions
The outer documentation frame should be **1440px wide**. Sections stack vertically with **64–100px gaps** between them. Each section frame fills the full 1440px width and hugs its content vertically.
### Full Foundations page skeleton
```javascript
async function createFoundationsPage() {
const page = figma.createPage();
page.name = 'Foundations';
await figma.setCurrentPageAsync(page);
// Batch the font loads — sequential awaits would serialize three IPC
// round-trips that can run in parallel.
await Promise.all([
figma.loadFontAsync({ family: 'Inter', style: 'Bold' }),
figma.loadFontAsync({ family: 'Inter', style: 'Medium' }),
figma.loadFontAsync({ family: 'Inter', style: 'Regular' }),
]);
// Root scroll frame
const root = figma.createAutoLayout('VERTICAL');
root.name = 'Foundations';
root.primaryAxisAlignItems = 'MIN';
root.counterAxisAlignItems = 'MIN';
root.itemSpacing = 80;
root.paddingTop = 80;
root.paddingBottom = 120;
root.paddingLeft = 80;
root.paddingRight = 80;
root.resize(1440, 1);
root.layoutSizingHorizontal = 'FIXED';
root.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
page.appendChild(root);
return { page, root };
}
```
---
## 3. Color Swatches (bound to variables)
Color swatches must be **bound to actual Figma variables** — never hardcode hex values in swatch fills. This keeps documentation in sync automatically when variable values change.
### Single color swatch
```javascript
/**
* Creates a single color swatch card (rectangle + variable name label).
* The swatch rectangle fill is bound to the provided variable.
*
* @param {FrameNode} parent - The auto-layout row to append to.
* @param {string} varName - Display name (e.g. "color/bg/primary").
* @param {Variable} variable - The Figma Variable object to bind to.
* @returns {FrameNode} The swatch frame.
*/
async function createColorSwatch(parent, varName, variable) {
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
const swatchFrame = figma.createAutoLayout('VERTICAL');
swatchFrame.name = `Swatch/${varName}`;
swatchFrame.primaryAxisAlignItems = 'MIN';
swatchFrame.counterAxisAlignItems = 'MIN';
swatchFrame.itemSpacing = 6;
swatchFrame.resize(88, 1);
swatchFrame.layoutSizingHorizontal = 'FIXED';
swatchFrame.fills = [];
// Color rectangle — bound to variable
const rect = figma.createRectangle();
rect.resize(88, 88);
rect.cornerRadius = 8;
const paint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } },
'color',
variable
);
rect.fills = [paint];
swatchFrame.appendChild(rect);
// Name label
const label = figma.createText();
label.fontName = { family: 'Inter', style: 'Regular' };
label.characters = varName.split('/').pop(); // show leaf name only
label.fontSize = 10;
label.fills = [{ type: 'SOLID', color: { r: 0.35, g: 0.35, b: 0.35 } }];
swatchFrame.appendChild(label);
label.layoutSizingHorizontal = 'FILL';
// Full path tooltip label (smaller, lighter)
const pathLabel = figma.createText();
pathLabel.fontName = { family: 'Inter', style: 'Regular' };
pathLabel.characters = varName;
pathLabel.fontSize = 9;
pathLabel.fills = [{ type: 'SOLID', color: { r: 0.6, g: 0.6, b: 0.6 } }];
swatchFrame.appendChild(pathLabel);
pathLabel.layoutSizingHorizontal = 'FILL';
parent.appendChild(swatchFrame);
return swatchFrame;
}
```
### Color section builder (primitives row + semantic grid)
```javascript
/**
* Creates a complete color documentation section with a section heading,
* a row of primitive swatches, and a grid of semantic swatches.
*
* @param {FrameNode} root - The root vertical stack frame.
* @param {Variable[]} primitiveVars - Variables from the Primitives collection.
* @param {Variable[]} semanticVars - Variables from the semantic Color collection.
*/
async function createColorSection(root, primitiveVars, semanticVars) {
await Promise.all([
figma.loadFontAsync({ family: 'Inter', style: 'Bold' }),
figma.loadFontAsync({ family: 'Inter', style: 'Regular' }),
]);
// Section container
const section = figma.createAutoLayout('VERTICAL');
section.name = 'Section/Colors';
section.itemSpacing = 24;
section.fills = [];
root.appendChild(section);
section.layoutSizingHorizontal = 'FILL';
// Section heading
const heading = figma.createText();
heading.fontName = { family: 'Inter', style: 'Bold' };
heading.characters = 'Colors';
heading.fontSize = 32;
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
section.appendChild(heading);
// Description
const desc = figma.createText();
desc.fontName = { family: 'Inter', style: 'Regular' };
desc.characters = 'Primitive color palette and semantic color tokens. Semantic tokens reference primitives — always use semantic tokens in components.';
desc.fontSize = 14;
desc.fills = [{ type: 'SOLID', color: { r: 0.4, g: 0.4, b: 0.4 } }];
section.appendChild(desc);
desc.layoutSizingHorizontal = 'FILL';
// Primitive swatches row
const primLabel = figma.createText();
primLabel.fontName = { family: 'Inter', style: 'Bold' };
primLabel.characters = 'Primitives';
primLabel.fontSize = 13;
primLabel.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
section.appendChild(primLabel);
const primRow = figma.createAutoLayout();
primRow.name = 'Primitives/Row';
primRow.itemSpacing = 12;
primRow.fills = [];
primRow.layoutWrap = 'WRAP';
section.appendChild(primRow);
primRow.layoutSizingHorizontal = 'FILL';
for (const v of primitiveVars) {
await createColorSwatch(primRow, v.name, v);
}
// Semantic swatches grid
if (semanticVars.length > 0) {
const semLabel = figma.createText();
semLabel.fontName = { family: 'Inter', style: 'Bold' };
semLabel.characters = 'Semantic';
semLabel.fontSize = 13;
semLabel.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
section.appendChild(semLabel);
const semRow = figma.createAutoLayout();
semRow.name = 'Semantic/Row';
semRow.itemSpacing = 12;
semRow.fills = [];
semRow.layoutWrap = 'WRAP';
section.appendChild(semRow);
semRow.layoutSizingHorizontal = 'FILL';
for (const v of semanticVars) {
await createColorSwatch(semRow, v.name, v);
}
}
return section;
}
```
---
## 4. Type Specimens
Typography specimens show each text style rendered at its actual size with a sample string, the style name, and its specifications.
### Single type specimen row
```javascript
/**
* Creates a single type specimen row: style name (small label) + sample text +
* specification line (family · style · size · line-height).
*
* @param {FrameNode} parent - The parent vertical stack.
* @param {string} styleName - The text style name (e.g. "Display Large").
* @param {string} fontFamily - Font family (e.g. "Inter").
* @param {string} fontStyle - Font style (e.g. "Bold").
* @param {number} fontSize - Font size in pixels.
* @param {number} lineHeight - Line height in pixels.
* @returns {FrameNode} The specimen row frame.
*/
async function createTypeSpecimen(parent, styleName, fontFamily, fontStyle, fontSize, lineHeight) {
await Promise.all([
figma.loadFontAsync({ family: fontFamily, style: fontStyle }),
figma.loadFontAsync({ family: 'Inter', style: 'Medium' }),
figma.loadFontAsync({ family: 'Inter', style: 'Regular' }),
]);
const row = figma.createAutoLayout('VERTICAL');
row.name = `Type/${styleName}`;
row.itemSpacing = 6;
row.paddingTop = 16;
row.paddingBottom = 16;
row.fills = [];
parent.appendChild(row);
row.layoutSizingHorizontal = 'FILL';
// Style name label (small, muted)
const nameText = figma.createText();
nameText.fontName = { family: 'Inter', style: 'Medium' };
nameText.characters = styleName;
nameText.fontSize = 11;
nameText.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
row.appendChild(nameText);
nameText.layoutSizingHorizontal = 'FILL';
// Sample text rendered in the actual style
const specimen = figma.createText();
specimen.fontName = { family: fontFamily, style: fontStyle };
specimen.characters = 'The quick brown fox jumps over the lazy dog';
specimen.fontSize = fontSize;
specimen.lineHeight = { value: lineHeight, unit: 'PIXELS' };
specimen.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
row.appendChild(specimen);
specimen.layoutSizingHorizontal = 'FILL';
// Specification line
const specs = figma.createText();
specs.fontName = { family: 'Inter', style: 'Regular' };
specs.characters = `${fontFamily} ${fontStyle} · ${fontSize}px · ${lineHeight}px line height`;
specs.fontSize = 11;
specs.fills = [{ type: 'SOLID', color: { r: 0.65, g: 0.65, b: 0.65 } }];
row.appendChild(specs);
specs.layoutSizingHorizontal = 'FILL';
// Divider line
const divider = figma.createRectangle();
divider.resize(1280, 1);
divider.fills = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }];
row.appendChild(divider);
divider.layoutSizingHorizontal = 'FILL';
return row;
}
```
### Typography section builder
```javascript
/**
* Creates a complete typography documentation section.
* Pass an array of style definitions; the function renders one specimen per entry.
*
* @param {FrameNode} root - Root vertical stack.
* @param {Array<{name, family, style, size, lineHeight}>} typeStyles - Style definitions.
*/
async function createTypographySection(root, typeStyles) {
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
const section = figma.createAutoLayout('VERTICAL');
section.name = 'Section/Typography';
section.itemSpacing = 0;
section.fills = [];
root.appendChild(section);
section.layoutSizingHorizontal = 'FILL';
const heading = figma.createText();
heading.fontName = { family: 'Inter', style: 'Bold' };
heading.characters = 'Typography';
heading.fontSize = 32;
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
section.appendChild(heading);
for (const ts of typeStyles) {
await createTypeSpecimen(section, ts.name, ts.family, ts.style, ts.size, ts.lineHeight);
}
return section;
}
```
---
## 5. Spacing Bars
Spacing bars show each spacing token as a filled rectangle whose width equals the spacing value. Shorter bars for small values, longer bars for large values — the visual encoding is immediate.
### Spacing bar row
```javascript
/**
* Creates a single spacing bar: a colored rectangle sized to the spacing value,
* with a label showing name + pixel value + code syntax.
*
* @param {FrameNode} parent - Parent vertical stack.
* @param {string} name - Token name (e.g. "spacing/sm").
* @param {number} value - Spacing value in pixels.
* @param {Variable} variable - Figma Variable to bind the width to.
* @param {string} codeSyntax - CSS variable string (e.g. "var(--spacing-sm)").
*/
async function createSpacingBar(parent, name, value, variable, codeSyntax) {
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
const row = figma.createAutoLayout();
row.name = `Spacing/${name}`;
row.counterAxisAlignItems = 'CENTER';
row.itemSpacing = 16;
row.fills = [];
parent.appendChild(row);
row.layoutSizingHorizontal = 'FILL';
// The bar rectangle — width bound to spacing variable
const bar = figma.createRectangle();
bar.resize(value, 16);
bar.cornerRadius = 3;
bar.fills = [{ type: 'SOLID', color: { r: 0.22, g: 0.47, b: 0.98 } }];
// Bind width to the spacing variable so it reflects the actual token value
if (variable) {
bar.setBoundVariable('width', variable);
}
row.appendChild(bar);
// Label: "spacing/sm 8px var(--spacing-sm)"
const label = figma.createText();
label.fontName = { family: 'Inter', style: 'Regular' };
label.characters = `${name} ${value}px ${codeSyntax}`;
label.fontSize = 12;
label.fills = [{ type: 'SOLID', color: { r: 0.35, g: 0.35, b: 0.35 } }];
row.appendChild(label);
return row;
}
```
### Spacing section builder
```javascript
/**
* Creates the full spacing documentation section.
*
* @param {FrameNode} root - Root vertical stack.
* @param {Array<{name, value, variable, codeSyntax}>} spacingTokens - Token definitions.
*/
async function createSpacingSection(root, spacingTokens) {
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
const section = figma.createAutoLayout('VERTICAL');
section.name = 'Section/Spacing';
section.itemSpacing = 12;
section.fills = [];
root.appendChild(section);
section.layoutSizingHorizontal = 'FILL';
const heading = figma.createText();
heading.fontName = { family: 'Inter', style: 'Bold' };
heading.characters = 'Spacing';
heading.fontSize = 32;
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
section.appendChild(heading);
for (const tok of spacingTokens) {
await createSpacingBar(section, tok.name, tok.value, tok.variable, tok.codeSyntax);
}
return section;
}
```
---
## 6. Shadow Cards (Elevation)
Elevation documentation shows cards with progressively stronger drop shadows, labeled with name and effect parameters.
### Single shadow card
```javascript
/**
* Creates a shadow card: a white rectangle with a drop shadow effect,
* labeled with the elevation name and shadow parameters.
*
* @param {FrameNode} parent - The horizontal row to append to.
* @param {string} name - Elevation name (e.g. "Shadow/Medium").
* @param {DropShadowEffect[]} effects - Array of Figma effect objects.
*/
async function createShadowCard(parent, name, effects) {
await Promise.all([
figma.loadFontAsync({ family: 'Inter', style: 'Regular' }),
figma.loadFontAsync({ family: 'Inter', style: 'Medium' }),
]);
const card = figma.createAutoLayout('VERTICAL');
card.name = `ShadowCard/${name}`;
card.primaryAxisAlignItems = 'CENTER';
card.counterAxisAlignItems = 'CENTER';
card.itemSpacing = 8;
card.paddingTop = 16;
card.paddingBottom = 16;
card.resize(120, 120);
card.layoutSizingHorizontal = 'FIXED';
card.layoutSizingVertical = 'FIXED';
card.cornerRadius = 8;
card.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
card.effects = effects;
parent.appendChild(card);
// Elevation name
const nameLabel = figma.createText();
nameLabel.fontName = { family: 'Inter', style: 'Medium' };
nameLabel.characters = name.split('/').pop();
nameLabel.fontSize = 12;
nameLabel.textAlignHorizontal = 'CENTER';
nameLabel.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.2, b: 0.2 } }];
card.appendChild(nameLabel);
// Effect parameters as small text
if (effects.length > 0) {
const e = effects[0];
if (e.type === 'DROP_SHADOW') {
const params = figma.createText();
params.fontName = { family: 'Inter', style: 'Regular' };
params.characters = `x:${e.offset.x} y:${e.offset.y}\nblur:${e.radius}`;
params.fontSize = 10;
params.textAlignHorizontal = 'CENTER';
params.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
card.appendChild(params);
}
}
return card;
}
```
### Shadow section builder
```javascript
/**
* Creates the full elevation/shadow documentation section.
*
* @param {FrameNode} root - Root vertical stack.
* @param {Array<{name, effects}>} shadowTokens - Shadow definitions.
*/
async function createShadowSection(root, shadowTokens) {
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
const section = figma.createAutoLayout('VERTICAL');
section.name = 'Section/Elevation';
section.itemSpacing = 24;
section.fills = [];
root.appendChild(section);
section.layoutSizingHorizontal = 'FILL';
const heading = figma.createText();
heading.fontName = { family: 'Inter', style: 'Bold' };
heading.characters = 'Elevation';
heading.fontSize = 32;
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
section.appendChild(heading);
// Cards row — extra top padding so shadows are visible
const row = figma.createAutoLayout();
row.name = 'Elevation/Row';
row.itemSpacing = 32;
row.paddingTop = 24;
row.paddingBottom = 40;
row.paddingLeft = 24;
row.paddingRight = 24;
row.fills = [{ type: 'SOLID', color: { r: 0.97, g: 0.97, b: 0.97 } }];
row.cornerRadius = 8;
section.appendChild(row);
row.layoutSizingHorizontal = 'FILL';
for (const tok of shadowTokens) {
await createShadowCard(row, tok.name, tok.effects);
}
return section;
}
```
---
## 7. Border Radius Demo
Border radius documentation shows rectangles at each corner radius value, labeled with the token name and pixel value.
### Single radius card
```javascript
/**
* Creates a single border radius card: a square with corner radius applied,
* labeled with the token name and pixel value.
*
* @param {FrameNode} parent - The horizontal row to append to.
* @param {string} name - Token name (e.g. "radius/md").
* @param {number} value - Corner radius in pixels (0 for none, 9999 for full).
* @param {Variable} [variable] - Optional Figma Variable to bind corner radius.
*/
async function createRadiusCard(parent, name, value, variable) {
await Promise.all([
figma.loadFontAsync({ family: 'Inter', style: 'Regular' }),
figma.loadFontAsync({ family: 'Inter', style: 'Medium' }),
]);
const wrapper = figma.createAutoLayout('VERTICAL');
wrapper.name = `Radius/${name}`;
wrapper.primaryAxisAlignItems = 'CENTER';
wrapper.counterAxisAlignItems = 'CENTER';
wrapper.itemSpacing = 8;
wrapper.fills = [];
wrapper.resize(96, 1);
wrapper.layoutSizingHorizontal = 'FIXED';
parent.appendChild(wrapper);
const rect = figma.createRectangle();
rect.resize(72, 72);
rect.fills = [{ type: 'SOLID', color: { r: 0.22, g: 0.47, b: 0.98, a: 0.15 } }];
rect.strokes = [{ type: 'SOLID', color: { r: 0.22, g: 0.47, b: 0.98 } }];
rect.strokeWeight = 1.5;
// Cap display value — 9999 is how Figma represents "full/pill"
const displayRadius = Math.min(value, 36);
rect.cornerRadius = displayRadius;
// Bind to variable if provided
if (variable) {
rect.setBoundVariable('cornerRadius', variable);
}
wrapper.appendChild(rect);
const nameLabel = figma.createText();
nameLabel.fontName = { family: 'Inter', style: 'Medium' };
nameLabel.characters = name.split('/').pop();
nameLabel.fontSize = 11;
nameLabel.textAlignHorizontal = 'CENTER';
nameLabel.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.2, b: 0.2 } }];
wrapper.appendChild(nameLabel);
const valueLabel = figma.createText();
valueLabel.fontName = { family: 'Inter', style: 'Regular' };
valueLabel.characters = value >= 9999 ? 'full' : `${value}px`;
valueLabel.fontSize = 10;
valueLabel.textAlignHorizontal = 'CENTER';
valueLabel.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
wrapper.appendChild(valueLabel);
return wrapper;
}
```
### Radius section builder
```javascript
/**
* Creates the full border radius documentation section.
*
* @param {FrameNode} root - Root vertical stack.
* @param {Array<{name, value, variable}>} radiusTokens - Radius token definitions.
*/
async function createRadiusSection(root, radiusTokens) {
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
const section = figma.createAutoLayout('VERTICAL');
section.name = 'Section/Radius';
section.itemSpacing = 24;
section.fills = [];
root.appendChild(section);
section.layoutSizingHorizontal = 'FILL';
const heading = figma.createText();
heading.fontName = { family: 'Inter', style: 'Bold' };
heading.characters = 'Border Radius';
heading.fontSize = 32;
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
section.appendChild(heading);
const row = figma.createAutoLayout();
row.name = 'Radius/Row';
row.itemSpacing = 24;
row.paddingTop = 24;
row.paddingBottom = 24;
row.paddingLeft = 24;
row.paddingRight = 24;
row.fills = [{ type: 'SOLID', color: { r: 0.97, g: 0.97, b: 0.97 } }];
row.cornerRadius = 8;
section.appendChild(row);
row.layoutSizingHorizontal = 'FILL';
for (const tok of radiusTokens) {
await createRadiusCard(row, tok.name, tok.value, tok.variable);
}
return section;
}
```
---
## 8. Documentation Alongside Components
Each component page should include a documentation frame directly on the canvas, placed to the left of the component set. This keeps docs and the component in sync without requiring a separate file.
### Component page documentation frame
```javascript
/**
* Creates the documentation frame for a component page: title, description,
* and usage notes, positioned at x=0 with the component set to its right.
*
* @param {PageNode} page - The component page (must already be current).
* @param {string} componentName - The component name.
* @param {string} description - What the component does and when to use it.
* @param {string[]} usageNotes - Bullet points for usage guidance.
* @returns {FrameNode} The documentation frame.
*/
async function createComponentDocFrame(page, componentName, description, usageNotes) {
await Promise.all([
figma.loadFontAsync({ family: 'Inter', style: 'Bold' }),
figma.loadFontAsync({ family: 'Inter', style: 'Regular' }),
]);
const doc = figma.createAutoLayout('VERTICAL');
doc.name = '_Doc';
doc.itemSpacing = 16;
doc.paddingTop = 40;
doc.paddingBottom = 40;
doc.paddingLeft = 40;
doc.paddingRight = 40;
doc.resize(360, 1);
doc.layoutSizingHorizontal = 'FIXED';
doc.fills = [];
doc.x = 0;
doc.y = 0;
page.appendChild(doc);
// Component name — large heading
const title = figma.createText();
title.fontName = { family: 'Inter', style: 'Bold' };
title.characters = componentName;
title.fontSize = 28;
title.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
doc.appendChild(title);
title.layoutSizingHorizontal = 'FILL';
// Description
const descText = figma.createText();
descText.fontName = { family: 'Inter', style: 'Regular' };
descText.characters = description;
descText.fontSize = 13;
descText.lineHeight = { value: 20, unit: 'PIXELS' };
descText.fills = [{ type: 'SOLID', color: { r: 0.35, g: 0.35, b: 0.35 } }];
doc.appendChild(descText);
descText.layoutSizingHorizontal = 'FILL';
// Divider
const divider = figma.createRectangle();
divider.resize(280, 1);
divider.fills = [{ type: 'SOLID', color: { r: 0.88, g: 0.88, b: 0.88 } }];
doc.appendChild(divider);
divider.layoutSizingHorizontal = 'FILL';
// Usage notes
if (usageNotes.length > 0) {
const usageHeading = figma.createText();
usageHeading.fontName = { family: 'Inter', style: 'Bold' };
usageHeading.characters = 'Usage';
usageHeading.fontSize = 13;
usageHeading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
doc.appendChild(usageHeading);
for (const note of usageNotes) {
const noteText = figma.createText();
noteText.fontName = { family: 'Inter', style: 'Regular' };
noteText.characters = `• ${note}`;
noteText.fontSize = 12;
noteText.lineHeight = { value: 18, unit: 'PIXELS' };
noteText.fills = [{ type: 'SOLID', color: { r: 0.4, g: 0.4, b: 0.4 } }];
doc.appendChild(noteText);
noteText.layoutSizingHorizontal = 'FILL';
}
}
return doc;
}
```
---
## 9. Critical Rules
1. **Bind swatches to variables** — use `setBoundVariableForPaint` for color fills, `setBoundVariable('width', ...)` for spacing bars, and `setBoundVariable('cornerRadius', ...)` for radius cards. Never hardcode values that have corresponding variables.
2. **Foundations page comes before component pages** — always insert it between the file structure separators and the first component page.
3. **Show both primitive and semantic layers** — if the system has a Primitives collection and a semantic Color collection, document both on the Foundations page with clear section labels.
4. **Page frame width = 1440px** — this is the convention across Simple DS, Polaris, and Material 3. Use it unless you detect a different existing convention via `get_metadata`.
5. **Section spacing = 64–80px** — the gap between color / typography / spacing / shadow / radius sections should be at minimum 64px so the page is scannable.
6. **Match existing page style** — if the target file uses emoji page name prefixes or a decorative separator style, carry that through to the Foundations page name.
7. **Include code syntax in labels** — where variables have code syntax set, display the CSS variable name in the swatch/bar label so developers can copy it directly.
references/error-recovery.md
> Part of the [figma-generate-library skill](../SKILL.md).
# Error Recovery Reference
Protocol for handling failures and incomplete runs in multi-call design-system work.
> **Design files only.** Every snippet here (including `figma.createPage()`) targets Figma Design files (`figma.com/design/...`). `figma.createPage()` throws in both FigJam (`figma.com/board/...`) and Slides (`figma.com/slides/...`).
---
## 1. Core Protocol
- If `safeToRetryWithoutCanvasRead` is `true`, fix the error and retry.
- If `false`, stop writes, read the canvas, determine what changed, then make changes.
However, in multi-step workflows, **previously successful calls** will have created state that persists. If a workflow is abandoned mid-way, nodes from earlier successful calls remain in the file. The cleanup and idempotency patterns in this document handle that scenario.
For **abandoned multi-step workflows** (where you need to roll back nodes from previous *successful* calls), use the cleanup protocol in Section 2.
---
## 2. ID-Based Cleanup
### Why name-prefix matching fails
A cleanup script that deletes "all nodes whose name starts with `Button`" will also delete nodes the user may have created manually with that name, or nodes from a previous approved phase. Name-based cleanup has no way to distinguish "orphan from a failed attempt" from "intentional user node."
Furthermore, variant names (`Size=Medium, Style=Primary, State=Default`) do not have consistent prefixes that are safe to target without also hitting legitimate nodes. Record every created ID in the state ledger as soon as its creation call succeeds.
### Cleanup using exact state-ledger IDs
Read the exact IDs from the state ledger, embed `scripts/cleanupOrphans.js`, and pass only those IDs:
```javascript
return await cleanupOrphans({
nodeIds: ['1:23', '1:47'],
variableIds: ['VariableID:1:8'],
collectionIds: ['VariableCollectionId:1:4'],
});
```
After cleanup, inspect the affected page and compare `removedIds` / `skippedIds` with the state ledger before retrying.
---
## 3. Idempotency Patterns: Check-Before-Create
Run an idempotency check at the start of every create operation. Resolve an ID from the state ledger first. If the ledger is unavailable, use a deterministic name scoped to the expected page, collection, and node type, then return the recovered ID.
### Check-before-create for a variable collection
```javascript
const COLLECTION_NAME = 'Color';
const allCollections = await figma.variables.getLocalVariableCollectionsAsync();
const existing = allCollections.find(c => c.name === COLLECTION_NAME);
if (existing) {
return {
collectionId: existing.id,
modeIds: existing.modes.map(m => ({ name: m.name, id: m.modeId })),
alreadyExisted: true,
};
}
// Create fresh
const collection = figma.variables.createVariableCollection(COLLECTION_NAME);
// Rename default mode, add second mode
collection.renameMode(collection.modes[0].modeId, 'Light');
const darkModeId = collection.addMode('Dark');
return {
collectionId: collection.id,
modeIds: [
{ name: 'Light', id: collection.modes[0].modeId },
{ name: 'Dark', id: darkModeId },
],
};
```
### Check-before-create for a page
```javascript
const PAGE_NAME = 'Button';
let page = figma.root.children.find(p => p.name === PAGE_NAME);
if (page) {
return { pageId: page.id, alreadyExisted: true };
}
page = figma.createPage();
page.name = PAGE_NAME;
return { pageId: page.id, alreadyExisted: false };
```
### Check-before-create for a component set
```javascript
const PAGE_ID = 'PAGE_ID_FROM_STATE';
const page = await figma.getNodeByIdAsync(PAGE_ID);
await figma.setCurrentPageAsync(page);
const existing = page.findAllWithCriteria({ types: ['COMPONENT_SET'] })
.filter(n => n.name === 'Button');
if (existing.length > 0) {
return {
componentSetId: existing[0].id,
alreadyExisted: true,
};
}
// ... proceed with creation
return { componentSetId: null, alreadyExisted: false };
```
---
## 4. State Ledger
### JSON Schema
Maintain a state ledger in your context (not in the Figma file) across calls. This is your source of truth for node IDs, completed steps, and pending validations.
```json
{
"runId": "ds-build-2024-001",
"scope": "single-component",
"step": "component-button/combine-variants",
"completedSteps": [
"discovery",
"foundations/verified",
"component-button/base",
"component-button/variants"
],
"entities": {
"collections": {
"primitives": "VariableCollectionId:1234:5678",
"color": "VariableCollectionId:1234:5679",
"spacing": "VariableCollectionId:1234:5680"
},
"variables": {
"color/bg/primary": "VariableId:2345:1",
"color/bg/secondary": "VariableId:2345:2",
"color/bg/disabled": "VariableId:2345:3",
"color/text/on-primary": "VariableId:2345:4",
"color/text/on-secondary": "VariableId:2345:5",
"color/text/disabled": "VariableId:2345:6",
"spacing/sm": "VariableId:2345:7",
"spacing/md": "VariableId:2345:8",
"spacing/lg": "VariableId:2345:9",
"radius/md": "VariableId:2345:10"
},
"modes": {
"color/light": "2345:1",
"color/dark": "2345:2"
},
"pages": { "Button": "0:3" },
"components": {
"Icon": "3456:1",
"Avatar": "3456:2",
"Button": "3456:3"
},
"componentSets": {
"Button": "4567:1"
}
},
"pendingValidations": ["Button:metadata", "Button:screenshot"]
}
```
### Persisting between calls
After every successful `use_figma` call:
1. Extract all IDs from the return value
2. Add them to the appropriate `entities` section of the ledger
3. Add the completed step to `completedSteps`
4. Remove from `pendingValidations` if this call validated something
5. Update `scope` and `step` to the current position
### Rehydrating at session start
If a conversation is interrupted and resumed, read the state ledger and verify key entities still exist:
```javascript
// Verify that critical nodes from the ledger still exist
const toVerify = {
'color-collection': 'VariableCollectionId:1234:5679',
'button-page': '0:3',
'button-componentset': '4567:1',
};
// Batch the lookups — awaiting getNodeByIdAsync per entry serializes the
// round-trips. Resolve them all in parallel with Promise.all, then walk the
// results.
const entries = Object.entries(toVerify);
const nodes = await Promise.all(
entries.map(([, id]) => figma.getNodeByIdAsync(id).catch(() => null))
);
const results = {};
for (let i = 0; i < entries.length; i++) {
const [label] = entries[i];
const node = nodes[i];
results[label] = node ? { found: true, name: node.name } : { found: false };
}
return results;
```
If any entity is missing, treat the phase that created it as incomplete and re-run from that checkpoint.
---
## 5. Resume Protocol
### Step 1: Inventory deterministic names
First list page IDs and names. Then run one page-scoped inspection per relevant page; do not switch through multiple pages in one call.
```javascript
const PAGE_ID = 'PAGE_ID_FROM_DISCOVERY';
const page = await figma.getNodeByIdAsync(PAGE_ID);
await figma.setCurrentPageAsync(page);
const nodes = page.findAllWithCriteria({
types: ['FRAME', 'COMPONENT', 'COMPONENT_SET'],
});
return nodes.map(node => ({
id: node.id,
type: node.type,
name: node.name,
...(node.type === 'COMPONENT' || node.type === 'COMPONENT_SET'
? { description: node.description }
: {}),
}));
```
Use separate read calls for local variable collections, variables, and styles. Filter each result against the deterministic names in the build plan.
### Step 2: Reconstruct state from inventory
Match names only within their expected entity type and parent page or collection. Add each confirmed ID to the state ledger, then mark the corresponding step as `completedSteps`.
Example mapping:
```
collection named 'Color' on the planned foundations page → entities.collections.color
variable named 'color/bg/primary' in that collection → entities.variables['color/bg/primary']
page named 'Button' → entities.pages.Button
component set named 'Button' on that page → entities.componentSets.Button
```
### Step 3: Identify the resume point
The resume point is the first step in the workflow that is NOT in `completedSteps`. If the inventory shows the Button component set exists but the pending validations list shows `'Button:screenshot'`, the resume point is the screenshot validation call, not re-creation.
Use the selected scope's acceptance checks to determine where to continue:
```
Discovery complete: relevant source and Figma assets inspected; conflicts resolved
Foundation dependency complete: required variables and styles exist and are verified
Component complete: requested component set exists with no pending validations
Full-library presentation complete: only the agreed pages and documentation exist and validate
```
---
## 6. Failure Taxonomy
### Recoverable Errors
These can be fixed and retried without affecting already-created entities:
| Category | Examples | Recovery |
|---|---|---|
| Layout errors | Variants stacked at (0,0), wrong padding values | Re-run the positioning step only |
| Naming issues | Typo in variant name, wrong casing | Resolve the node by its state-ledger ID and update `name` |
| Missing property wiring | `componentPropertyReferences` not set | Find component set by ID, re-run the property wiring step |
| Variable binding omission | A fill was hardcoded instead of bound | Resolve the node by its state-ledger ID and re-bind the fill |
| Wrong variable bound | Bound to wrong variable ID | Re-bind with correct variable ID |
| Text not visible | Font not loaded before text write | Call `listAvailableFontsAsync()` to verify the font exists, then re-run text creation with `loadFontAsync` |
| Script timeout | Script exceeded time limit before completing | Follow `safeToRetryWithoutCanvasRead`; if `false`, inspect before reducing scope and retrying |
### Structural Corruption (Requires Rollback or Restart)
These errors leave the file in a state where continuing forward is unreliable:
| Category | Examples | Recovery |
|---|---|---|
| Component cycle | A component instance was accidentally nested inside itself | Fully clean up the affected component and restart from its first in-scope creation step |
| combineAsVariants with non-components | Mixed node types passed to combineAsVariants, causing unexpected merges | Remove the malformed component set, re-run from variant creation |
| Variable collection ID drift | Collection was deleted and re-created, old IDs in state ledger are stale | Re-run the affected foundation step; update all dependent IDs in the ledger |
| In-scope page deletion | A required page was deleted after component sets were created on it | Re-create that page and re-run only affected component creations |
| Mode limit exceeded | `addMode` threw because the plan is Starter or Professional | Redesign the in-scope collection architecture to fit mode limits, restart that foundation step |
**Recovery from structural corruption**: run `cleanupOrphans` with the exact state-ledger IDs for the affected scoped step, then restart that step. Do NOT attempt to patch corrupted structure in-place.
---
## 7. Common Error Table
| Error message | Likely cause | Fix |
|---|---|---|
| `"Cannot create component from node"` | Tried to call `createComponentFromNode` on a node inside a component | Create a fresh component instead: `figma.createComponent()` |
| `"in addMode: Limited to N modes only"` | Plan mode limit hit (Starter=1, Professional=4) | Redesign to use fewer modes or upgrade plan |
| `"setCurrentPageAsync: page does not exist"` | Page was deleted or wrong ID | Re-create the page using the idempotency pattern |
| `"Cannot read properties of null"` | `getNodeByIdAsync` returned null — node was deleted | Run the resume protocol to find what exists, update state ledger |
| `"Expected nodes to be component nodes"` | Passed a non-ComponentNode to `combineAsVariants` | Filter the array: `nodes.filter(n => n.type === 'COMPONENT')` |
| `"in createVariable: Cannot create variable"` | Collection was deleted or ID is wrong | Verify collection exists with `getVariableCollectionByIdAsync` |
| `"font not loaded"` | Called a text property setter without `loadFontAsync` first | Call `await figma.listAvailableFontsAsync()` to discover available fonts and verify the font name, then `await figma.loadFontAsync({ family, style })` before the text operation |
| `"Cannot set properties of a read-only array"` | Tried to mutate fills/strokes in-place | Clone first: `const fills = JSON.parse(JSON.stringify(node.fills))` |
| `"Expected RGBA color"` | Color value out of 0–1 range | Divide RGB 0–255 values by 255: `{ r: 65/255, g: 85/255, b: 143/255 }` |
| `"Cannot add children to a non-parent node"` | Tried to append a child to a leaf node (text, rect) | Ensure the parent is a FrameNode, ComponentNode, or GroupNode |
| `"in combineAsVariants: nodes must be in the same parent"` | Components are on different pages | Move all components to the same page before combining |
| `"Script exceeded time limit"` | Loop creating too many nodes in one call | Split the work: create N/2 variants per call |
| Component set deletes itself | Tried to create a component set with no children | `combineAsVariants` requires at least 1 node — always pass 1+ |
| `addComponentProperty` returns unexpected name | This is normal — `BOOLEAN`/`TEXT`/`INSTANCE_SWAP` get `#id:id` suffix | Save the returned key immediately and use that, not the input name |
---
## 8. Recovery Guidance by Asset Type
### Variable creation fails
- If `safeToRetryWithoutCanvasRead` is `true`, fix the error and retry.
- If `false`, inventory variables, determine what changed, then resume idempotently. Do not build an in-scope dependent asset until its required variables are correct.
**The most common variable-creation failure:** script timeout when creating many variables. Fix: batch variable creation — create at most 20–30 variables per call.
### In-scope page or file structure fails
This section applies only when the selected scope includes those pages or documentation. Symptoms: some agreed pages exist, others are missing; requested documentation frames are incomplete.
Recovery steps:
1. Identify which pages were successfully created by their deterministic names and state-ledger IDs
2. Mark remaining pages as pending and create them in subsequent calls
3. If an in-scope documentation frame is malformed, pass its exact state-ledger ID to `cleanupOrphans`, then recreate it
Page failures rarely require foundation rollback unless the page structure itself is corrupted.
### Component creation fails
This is the most common failure mode in long builds. Previous successful calls persist. If `safeToRetryWithoutCanvasRead` is `false`, inspect the component page and state ledger before resuming idempotently.
**Idempotency for component properties (Call 6 retry):**
```javascript
const candidate = await figma.getNodeByIdAsync(COMPONENT_SET_ID);
if (!candidate || candidate.type !== 'COMPONENT_SET') throw new Error('Expected a component set');
const existingDefs = candidate.componentPropertyDefinitions;
const labelKey = Object.keys(existingDefs).find(key => key.startsWith('Label#'))
?? candidate.addComponentProperty('Label', 'TEXT', 'Button');
```
### In-scope QA or Code Connect fails
This section applies only when the selected scope includes the relevant audit or mapping. These failures do not corrupt completed component work. Common failures:
- **Accessibility audit finds contrast failures:** do not attempt auto-fix. Report the specific variable IDs and token names that fail, then ask the user which value to update.
- **Naming audit finds duplicates:** list all duplicates with their `key` values, ask user which to keep, then remove the duplicates.
- **Code Connect mapping fails:** treat as incomplete, not broken. Continue and leave as pending.
references/naming-conventions.md
> Part of the [figma-generate-library skill](../SKILL.md).
# Naming Conventions Reference
This reference documents every naming convention used in the figma-generate-library workflow. Cover all naming decisions in order: variables, components, pages, variants, styles, separators, status indicators. The last section explains when to match an existing file's conventions vs. using the defaults here.
---
## 1. Variable Naming
### Slash hierarchy (the universal pattern)
All Figma variables use slash-separated paths. The slash creates visual grouping in the Variables panel and maps directly to the token hierarchy in code.
```
{category}/{subcategory}/{role}
```
Real examples from Simple DS and Material 3:
```
color/bg/primary
color/bg/secondary
color/text/primary
color/text/muted
color/border/default
color/border/focus
color/feedback/error
color/feedback/success
spacing/xs
spacing/sm
spacing/md
spacing/lg
spacing/xl
spacing/2xl
radius/none
radius/sm
radius/md
radius/lg
radius/full
typography/body/font-size
typography/body/line-height
typography/heading/font-size
typography/heading/font-weight
```
### Primitives collection
Primitive variables hold raw values and are **not** exposed to consumers (scope = `[]`). They use a flat `{family}/{step}` format matching the color scale convention from Simple DS:
```
blue/50
blue/100
blue/200
...
blue/900
gray/50
gray/100
...
gray/900
red/500
green/500
```
Step numbers follow the convention of the target codebase. If the codebase uses `100–900`, use that. If it uses `50–950`, use that. If there is no codebase convention, use `100–900` in increments of 100.
### Semantic collection
Semantic variables alias primitives. They use the role-based `{category}/{role}` or `{category}/{subcategory}/{role}` pattern:
```
color/bg/primary → alias: primitives/white (light), primitives/gray/900 (dark)
color/bg/secondary → alias: primitives/gray/100 (light), primitives/gray/800 (dark)
color/text/primary → alias: primitives/gray/900 (light), primitives/white (dark)
color/text/secondary → alias: primitives/gray/600 (light), primitives/gray/400 (dark)
color/border/default → alias: primitives/gray/200 (light), primitives/gray/700 (dark)
```
**Rule:** Semantic variables must never hold raw hex values — they always alias a primitive. If you need a new color value, create the primitive first, then create the semantic alias.
### Casing
**Default:** Use **lowercase** with forward slashes: `color/bg/primary`, `spacing/2xl`.
**When to deviate:**
- If the existing file uses PascalCase (e.g., Material 3 uses `Schemes/Primary`) — match it.
- If the design team prefers PascalCase for readability in the Variables panel — acceptable as long as the code syntax is separately defined and uses the platform-correct case.
- Mode names can use spaces and mixed case (e.g., `SDS Light`, `Mode 1 → Light`) — these are labels, not identifiers.
**Never:** camelCase inside variable names (`colorBgPrimary` as a Figma name is wrong — that belongs in Android code syntax only). Never use spaces inside a path segment: `color/bg primary` is wrong; `color/bg/primary` is correct.
**Key distinction:** The casing rule applies to *Figma variable names*. Code syntax names follow *platform conventions* regardless of the Figma name case — see §9 for the full picture.
---
## 2. Component Naming
### Main components: PascalCase, no prefix
Published components intended for library consumers use plain PascalCase names:
```
Button
Input
Checkbox
Toggle
Avatar
Badge
Card
Dialog
Tooltip
Banner
```
Do not use a namespace prefix for public components (e.g., do not name them `DS/Button` or `sds-Button`). Slashes in component names create nested grouping in the Assets panel, which is correct for sub-components but not for top-level public components.
### Sub-components: underscore prefix + slash namespace
Internal sub-components that are NOT meant for library consumers use the `_` prefix. This hides them from the Assets panel by default and signals to other designers that they should not be used directly.
```
_Button/Slot (internal icon slot for Button)
_Input/Indicator (internal state indicator for Input)
_Badge/Dot (internal dot sub-component of Badge)
_Parts/Avatar.Status (UI3 pattern: _Parts/{ParentName}.{SubPart})
_Slider/Handle (UI3 pattern: _{ParentName}/{SubPart})
```
Pattern rules:
- Use `_` prefix for ALL internal sub-components — no exceptions.
- Use slash namespacing to group sub-components under their parent: `_Button/IconSlot`.
- For sub-components shared by multiple parents, use `_Parts/{ComponentName}.{SubPart}`.
### Private documentation components
Components used only for internal documentation (not for production use) use the `.` prefix:
```
.ExampleCard
.GuidelineHeader
.DemoFrame
```
This hides them from consumers while keeping them accessible on the canvas.
---
## 3. Page Naming
Five reference design systems use three distinct naming patterns. Choose one pattern and apply it consistently across all pages in the file.
### Pattern 1: Plain names (Simple DS, Material 3, Polaris)
The most common pattern. Clean, readable, no decoration.
```
Cover
---
Foundations
Icons
---
Accordion
Avatars
Buttons
Cards
Dialog
Inputs
Menu
---
Utilities
Component Playground
```
Use this pattern when starting from scratch or when the target file already uses this style.
### Pattern 2: Emoji prefix + status (UI3 Library)
The most expressive pattern. The page name encodes asset type, design status, and code readiness.
Anatomy: `[Asset Type Emoji] [Optional FPL Label] [Status Circle] Component Name [Code Status Bracket]`
| Segment | Values |
|---------|--------|
| Asset type | Component pages use the C-flag emoji; pattern pages use the P-flag emoji |
| Design status | Green circle = Ready, Yellow circle = WIP, Red circle = Do not use |
| Code status | (none) = Ready in code, `[beta]` = Beta, `[future]` = Not yet built |
Examples:
```
Overview
Status Key
---
FPL COMPONENTS
[C-flag] FPL [Green] Buttons
[C-flag] FPL [Green] Inputs
[C-flag] FPL [Yellow] Popovers [future]
---
UI3 COMPONENTS
[C-flag] [Green] Comments
---
PATTERNS
[P-flag] [Green] Editor / Layers
---
[Book] Cover
[Headstone] Deprecated
```
Use this pattern only when building a large, multi-team design system where lifecycle tracking is needed, or when the target file already uses it.
### Pattern 3: Emoji prefix (Shop Minis)
A lighter version of the UI3 pattern without status circles.
```
📔 Cover
ℹ️ About
🚀 Getting started
——— THEME ———
Color
Typography
Spacing
——— COMPONENTS ———
Button
Input
Card
```
Use this pattern when the target file already uses emoji prefixes but does not need lifecycle tracking.
### Universal rules (all patterns)
- **Cover** is always first.
- **Separator pages** come before and after each logical section.
- **Foundation/token pages** always come before component pages.
- **Utility and internal pages** always come last.
- Pick one convention and do not mix patterns within a file.
---
## 4. Variant Naming
### Property=Value format
All component variant properties and their values use `Property=Value` format in the Figma component set:
```
Size=Small, Style=Primary, State=Default
Size=Medium, Style=Secondary, State=Hover
Size=Large, Style=Ghost, State=Disabled
```
Actual property names match code prop names where possible:
| Figma Property | Code Prop Equivalent |
|---------------|---------------------|
| `Size` | `size` |
| `Style` / `Variant` | `variant` |
| `State` | Typically controlled by `:hover`, `:focus`, `:disabled` in CSS, but `state` in some systems |
| `Type` | `type` |
| `Disabled` | `disabled` (boolean) |
| `Icon` | `icon` (boolean or instance swap) |
### Property value casing
Property values use **Title Case** in Figma (to be readable in the Variants panel), mapping to lowercase in code:
| Figma value | Code value |
|-------------|-----------|
| `Small` | `"small"` / `"sm"` |
| `Medium` | `"medium"` / `"md"` |
| `Large` | `"large"` / `"lg"` |
| `Primary` | `"primary"` |
| `Disabled` | `disabled` (boolean prop) |
| `Default` | *(typically the absent/unset case)* |
### Boolean properties
Boolean component properties in Figma use `true` / `false` as values (Figma's native boolean), not `Yes` / `No` or `On` / `Off`.
---
## 5. Style Naming (Text and Effect Styles)
### Text styles: category/name
```
Display/Large
Display/Medium
Display/Small
Heading/1
Heading/2
Heading/3
Body/Large
Body/Medium
Body/Small
Label/Large
Label/Small
Code/Inline
```
The category segment maps to the typographic role. Use the same category names as the codebase's typography scale where possible.
### Effect styles (shadows): category/name
```
Shadow/None
Shadow/Subtle
Shadow/Medium
Shadow/Strong
Shadow/Overlay
Elevation/0
Elevation/1
Elevation/2
Elevation/3
Elevation/4
Elevation/5
```
Use `Shadow/` for named semantic shadows. Use `Elevation/N` for Material Design-style numbered elevation levels.
---
## 6. Separator Pages
Separator pages are empty pages whose sole purpose is to create visual breaks in the Figma page panel. Two conventions:
| Convention | Example | Used by |
|------------|---------|---------|
| Three dashes | `---` | Simple DS, UI3, Polaris, Material 3 |
| Decorated text | `——— COMPONENTS ———` | Shop Minis |
The three-dash convention (`---`) is the most common and the default for new files. Use it unless the target file uses the decorated-text style.
**Where to place separators:**
```
Cover
--- ← after cover
Foundations
Icons
--- ← before components
[component pages]
--- ← before utilities
Utilities
```
---
## 7. Status Indicators (UI3 Emoji System)
The UI3 Library uses colored circle emojis in page names to communicate design readiness at a glance. This system is optional but powerful for large teams.
| Emoji | Meaning | When to use |
|-------|---------|-------------|
| Green circle | Ready / Approved | Design is stable, reviewed, and safe to use |
| Yellow circle | WIP / In Progress | Design is being actively worked on, may change |
| Red circle | Do not use | Not ready, do not reference; may be deprecated |
Code readiness is communicated via brackets appended to the component name:
| Bracket | Meaning |
|---------|---------|
| (none) | Component is implemented in code and stable |
| `[beta]` | Component is in code but not yet stable (~3 weeks from ready) |
| `[future]` | Not yet implemented in code |
**Documentation status (within component pages):**
If building a UI3-style system, each documentation frame gets a status banner with one of these labels:
- `APPROVED` — fully vetted
- `READY FOR REVIEW` — awaiting sign-off
- `WORK IN PROGRESS` — actively being designed
- `NEEDS UPDATE` — outdated, requires revision
- `DO NOT REFERENCE` — should not be used
This system is only recommended for large, multi-team systems where lifecycle tracking provides real value. For smaller systems, skip the emoji status indicators and use plain page names.
---
## 8. When to Match Existing vs. Use Defaults
**Always inspect before naming anything.** Run `get_metadata` or `inspectFileStructure` to discover existing conventions before creating any pages or variables.
### Match the existing file when:
- The file already has pages with a consistent naming pattern (emoji prefixes, separator style, casing).
- The file already has variable collections with an established naming scheme.
- The file was started by a design team and carries intentional decisions.
- Any existing component names use a specific pattern (PascalCase, kebab-case, namespace prefixes).
### Use the defaults from this document when:
- Starting a brand-new Figma file with no existing content.
- The existing conventions are inconsistent (mix of styles = no convention to match).
- The user explicitly asks for a fresh design system following best practices.
### When code and Figma disagree:
If the codebase uses `button-primary` but Figma has a component named `Button`, do not rename the Figma component. Instead:
- Keep the Figma name as `Button` (PascalCase, human-readable).
- Set variable code syntax to match the exact CSS token name from the codebase.
- Set Code Connect source path to the actual code file and use the exact code component name.
**The rule:** Figma names are for designers; code syntax and Code Connect source paths carry the exact code identifiers. These two identity systems operate in parallel.
---
## 9. Figma Variable Names vs Code Names — The Full Picture
This is one of the most misunderstood areas. Figma names and code names follow **different conventions on purpose** — they serve different audiences and live in different environments.
### Why they differ
| | Figma variable name | Code syntax (WEB) |
|---|---|---|
| **Audience** | Designers in the Variables panel | Developers in CSS/Swift/Kotlin |
| **Separator** | `/` (slash) — creates visual grouping in Figma UI | `-` (hyphen) — required by CSS custom property syntax |
| **Case** | lowercase (or PascalCase for display — see below) | kebab-case for CSS; camelCase for JS/Android |
| **Depth** | 2–4 levels | Flat for CSS; dot-notation for JS |
| **Namespace** | Implicit (by collection) | Explicit prefix (`--p-`, `--md-`, `--cds-`) |
### The transformation
```
Figma variable name Code syntax (WEB)
────────────────── ─────────────────
color/bg/primary → var(--color-bg-primary)
spacing/xs → var(--spacing-xs)
radius/md → var(--radius-md)
typography/body/font-size → var(--typography-body-font-size)
Pattern: replace "/" with "-", wrap in var(--)
**CRITICAL: The `var()` wrapper is REQUIRED for WEB code syntax.** Figma expects the full CSS function syntax — not just the property name. If you set `--color-bg-primary` (without `var()`), Dev Mode will show raw hex values instead of the variable reference. Always set `var(--color-bg-primary)`.
```
```
Figma variable name Code syntax (ANDROID)
────────────────── ─────────────────────
color/bg/primary → colorBgPrimary
spacing/xs → spacingXs
radius/md → radiusMd
Pattern: replace "/" with "", capitalize each word after first
```
```
Figma variable name Code syntax (iOS)
────────────────── ─────────────────
color/bg/primary → Color.bgPrimary
spacing/xs → Spacing.xs
radius/md → Radius.md
Pattern: first segment becomes class name, remainder becomes property (camelCase)
```
### Real-world examples from the 5 reference files
| File | Figma variable name | WEB code syntax | ANDROID code syntax |
|------|--------------------|-----------------|--------------------|
| Simple DS | `color/bg/primary` | `var(--color-bg-primary)` | `colorBgPrimary` |
| Simple DS | `spacing/sm` | `var(--spacing-sm)` | `spacingSm` |
| Material 3 | `Schemes/Primary` | `var(--md-sys-color-primary)` | `colorPrimary` |
| Material 3 | `Corner/Extra-small` | `var(--md-sys-shape-corner-extra-small)` | `shapeCornerExtraSmall` |
| Polaris | `color/bg/surface` | `var(--p-color-bg-surface)` | — |
**Key observation from Material 3:** The Figma name `Schemes/Primary` uses PascalCase with a space, but the WEB code syntax is `var(--md-sys-color-primary)` — entirely kebab-case with a vendor prefix `md-sys-`. The Figma name and the code syntax bear almost no resemblance. This is intentional and common in mature design systems.
### Casing in Figma: lowercase is default, PascalCase is valid for display
The guideline to use lowercase is a default, not a universal rule. Evidence from real files:
| File | Figma case | Code output case | Why |
|------|-----------|------------------|-----|
| Simple DS | `color/bg/primary` (lowercase) | `var(--color-bg-primary)` | Direct mapping — simple |
| Material 3 | `Schemes/Primary` (PascalCase) | `var(--md-sys-color-primary)` | PascalCase reads better in Variables panel; code name is independently defined |
| Polaris | `color/bg/surface` (lowercase) | `var(--p-color-bg-surface)` | Direct mapping with vendor prefix |
**Rule:** Use lowercase when the Figma name will map directly to the CSS name. Use PascalCase (or match existing file) when the design system has human-readable variable names that are distinct from the technical code names.
### When the codebase doesn't use CSS custom properties
Some JavaScript-first systems (Chakra, Ant Design, MUI) don't use CSS `var(--...)` at all. Their tokens live in JS theme objects:
```
Chakra: colors.gray[500] → JS: theme.colors.gray[500]
Ant: colorPrimary → JS: token.colorPrimary
MUI: palette.primary.main → JS: theme.palette.primary.main
```
In these cases, set WEB code syntax to the JS property path rather than a CSS variable:
```javascript
// For a JS-object-based system like Chakra:
v.setVariableCodeSyntax('WEB', 'colors.gray.500');
// For Ant Design:
v.setVariableCodeSyntax('WEB', 'colorPrimary');
```
### Hierarchy depth: match the codebase
The number of slash levels should mirror the codebase's nesting depth:
| Codebase pattern | Figma depth | Example |
|-----------------|------------|---------|
| `--primary` (flat) | 1–2 levels | `color/primary` |
| `--color-bg-surface` (3-part) | 3 levels | `color/bg/surface` |
| `--md-sys-color-primary` (vendor + 3-part) | 3 levels (vendor prefix goes in code syntax only) | `color/primary` |
| `theme.palette.primary.main` (4-part) | 3–4 levels | `color/palette/primary/main` |
**Important:** Vendor prefixes (`--p-`, `--md-sys-`, `--cds-`) belong in the **code syntax**, not the Figma variable name. The Figma name `color/bg/surface` + code syntax `var(--p-color-bg-surface)` is the correct pattern.
### Action at discovery time
During Phase 0 discovery, capture both sides of the mapping explicitly:
```
For each token found in the codebase:
CSS variable: --sds-color-background-brand-default
Figma name: color/bg/brand/default (slash hierarchy, no vendor prefix)
WEB syntax: var(--sds-color-background-brand-default) (exact CSS name)
ANDROID syntax: sdsColorBackgroundBrandDefault (camelCase)
iOS syntax: Color.backgroundBrandDefault (dot-notation)
```
Store this mapping in the state ledger. Use it when calling `setVariableCodeSyntax` in Phase 1. Never derive the code syntax from the Figma name if you have the original CSS variable name — always use the original.
references/token-creation.md
> Part of the [figma-generate-library skill](../SKILL.md).
# Token Creation Reference
This document covers Phase 1: creating variable collections, modes, primitives, semantic aliases, scopes, code syntax, styles, and validation. All code is copy-paste ready for `use_figma`.
---
## 1. Collection Architecture
Choose the pattern that matches your token count and complexity:
### Simple Pattern (< 50 tokens)
One collection, 2 modes. Appropriate for small projects or brand kits.
```
Collection: "Tokens" modes: ["Light", "Dark"]
color/bg/primary → Light: #FFFFFF, Dark: #1A1A1A
spacing/sm = 8
```
### Standard Pattern (50–200 tokens) — Recommended Starting Point
Separate primitives from semantics. The real-world reference is Figma's Simple Design System (SDS): 7 collections, 368 variables, light/dark modes on semantic colors, single-mode primitives.
```
Collection: "Primitives" modes: ["Value"] ← raw hex values, no modes
blue/500 = #3B82F6
gray/900 = #111827
white/1000 = #FFFFFF
Collection: "Color" modes: ["Light", "Dark"] ← aliases to Primitives
color/bg/primary → Light: alias Primitives/white/1000, Dark: alias Primitives/gray/900
color/text/primary → Light: alias Primitives/gray/900, Dark: alias Primitives/white/1000
Collection: "Spacing" modes: ["Value"]
spacing/xs = 4, spacing/sm = 8, spacing/md = 16, spacing/lg = 24, spacing/xl = 32
Collection: "Typography Primitives" modes: ["Value"]
family/sans = "Inter", scale/01 = 12, scale/02 = 14, scale/03 = 16, weight/regular = 400
Collection: "Typography" modes: ["Value"] ← aliases to Typography Primitives
body/font-family → alias family/sans
body/size-md → alias scale/03
```
### Advanced Pattern (200+ tokens) — M3 Model
Multiple semantic collections, 4–8 modes. Use when you need light/dark × contrast × brand or responsive breakpoints.
```
Collection: "M3" modes: ["Light", "Dark", "Light High Contrast", "Dark High Contrast", ...]
Collection: "Typeface" modes: ["Baseline", "Wireframe"]
Collection: "Typescale" modes: ["Value"] ← aliases into Typeface
Collection: "Shape" modes: ["Value"]
```
Key insight from M3: ALL 196 semantic color variables live in a SINGLE collection with 8 modes. Switching a frame's mode once updates every color simultaneously.
---
## 2. Creating Collections + Modes
### Creating a Primitives Collection
```javascript
// Create the collection
const primColl = figma.variables.createVariableCollection("Primitives");
// Rename the default "Mode 1" to "Value"
primColl.renameMode(primColl.modes[0].modeId, "Value");
const valueMode = primColl.modes[0].modeId;
return {
collectionId: primColl.id,
modeId: valueMode,
name: primColl.name
};
```
### Creating a Semantic Color Collection with Light/Dark Modes
```javascript
const colorColl = figma.variables.createVariableCollection("Color");
// Rename default "Mode 1" to "Light"
colorColl.renameMode(colorColl.modes[0].modeId, "Light");
const lightModeId = colorColl.modes[0].modeId;
// Add "Dark" mode — requires Professional plan or higher
// Throws "in addMode: Limited to N modes only" on Starter plan
const darkModeId = colorColl.addMode("Dark");
return {
collectionId: colorColl.id,
lightModeId,
darkModeId
};
```
**Mode plan limits:** Starter = 1 mode, Professional = 4 modes, Organization/Enterprise = 40 modes. If `addMode` throws, the file is on a Starter plan — tell the user and ask how to proceed.
### Creating a Spacing Collection (single mode)
```javascript
const spacingColl = figma.variables.createVariableCollection("Spacing");
spacingColl.renameMode(spacingColl.modes[0].modeId, "Value");
const valueMode = spacingColl.modes[0].modeId;
return {
collectionId: spacingColl.id,
modeId: valueMode
};
```
---
## 3. Creating All Variable Types
### hex → {r, g, b} Conversion Helper
Colors in the Figma Plugin API are 0–1 range, not 0–255. Embed this helper in any script that creates color variables:
```javascript
function hexToRgb(hex) {
const clean = hex.replace('#', '');
return {
r: parseInt(clean.substring(0, 2), 16) / 255,
g: parseInt(clean.substring(2, 4), 16) / 255,
b: parseInt(clean.substring(4, 6), 16) / 255
};
}
// With alpha channel (for semi-transparent primitives like Black/200 at 10%):
function hexToRgba(hex) {
const clean = hex.replace('#', '');
const hasAlpha = clean.length === 8;
return {
r: parseInt(clean.substring(0, 2), 16) / 255,
g: parseInt(clean.substring(2, 4), 16) / 255,
b: parseInt(clean.substring(4, 6), 16) / 255,
a: hasAlpha ? parseInt(clean.substring(6, 8), 16) / 255 : 1
};
}
// Usage:
// hexToRgb('#3B82F6') → {r: 0.231, g: 0.510, b: 0.965}
// hexToRgb('#14AE5C') → {r: 0.078, g: 0.682, b: 0.361}
// hexToRgba('#0c0c0d1a') → {r: 0.047, g: 0.047, b: 0.051, a: 0.102}
```
### Creating Primitive Color Variables (Real SDS Data)
This creates a subset of the Simple Design System's `Color Primitives` collection (Blue family, from the Standard pattern used by real design systems):
```javascript
function hexToRgb(hex) {
const c = hex.replace('#', '');
return { r: parseInt(c.slice(0,2),16)/255, g: parseInt(c.slice(2,4),16)/255, b: parseInt(c.slice(4,6),16)/255 };
}
// Get the Primitives collection created in the previous step
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const primColl = collections.find(c => c.name === 'Primitives');
if (!primColl) throw new Error("Primitives collection not found — run collection creation first");
const valueMode = primColl.modes[0].modeId;
// Define primitives — use real values from your codebase
const primitiveColors = [
// Blue scale
{ name: 'blue/100', hex: '#EFF6FF' },
{ name: 'blue/200', hex: '#DBEAFE' },
{ name: 'blue/300', hex: '#93C5FD' },
{ name: 'blue/400', hex: '#60A5FA' },
{ name: 'blue/500', hex: '#3B82F6' },
{ name: 'blue/600', hex: '#2563EB' },
{ name: 'blue/700', hex: '#1D4ED8' },
{ name: 'blue/800', hex: '#1E40AF' },
{ name: 'blue/900', hex: '#1E3A8A' },
// Gray scale
{ name: 'gray/100', hex: '#F9FAFB' },
{ name: 'gray/200', hex: '#F3F4F6' },
{ name: 'gray/300', hex: '#D1D5DB' },
{ name: 'gray/400', hex: '#9CA3AF' },
{ name: 'gray/500', hex: '#6B7280' },
{ name: 'gray/600', hex: '#4B5563' },
{ name: 'gray/700', hex: '#374151' },
{ name: 'gray/800', hex: '#1F2937' },
{ name: 'gray/900', hex: '#111827' },
// White / Black
{ name: 'white/1000', hex: '#FFFFFF' },
{ name: 'black/1000', hex: '#000000' },
];
const created = [];
for (const { name, hex } of primitiveColors) {
const v = figma.variables.createVariable(name, primColl, 'COLOR');
v.setValueForMode(valueMode, hexToRgb(hex));
// Primitives: EMPTY scopes (hidden from all pickers — designers use semantics)
v.scopes = [];
// Code syntax from the actual CSS variable name
v.setVariableCodeSyntax('WEB', `var(--color-${name.replace('/', '-')})`);
created.push({ name, id: v.id });
}
return { created, count: created.length };
```
**Critical scope rule for primitives:** Set `v.scopes = []`. This hides primitives from every picker. Designers should only see semantic tokens. The exception is semi-transparent overlay primitives (Black/White with alpha) — those get `["EFFECT_COLOR"]` so they appear in shadow pickers.
### Creating FLOAT Variables (Spacing, Radius, Font Size)
```javascript
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const spacingColl = collections.find(c => c.name === 'Spacing');
if (!spacingColl) throw new Error("Spacing collection not found");
const valueMode = spacingColl.modes[0].modeId;
const spacingTokens = [
{ name: 'spacing/xs', value: 4, scope: 'GAP', cssVar: '--spacing-xs' },
{ name: 'spacing/sm', value: 8, scope: 'GAP', cssVar: '--spacing-sm' },
{ name: 'spacing/md', value: 16, scope: 'GAP', cssVar: '--spacing-md' },
{ name: 'spacing/lg', value: 24, scope: 'GAP', cssVar: '--spacing-lg' },
{ name: 'spacing/xl', value: 32, scope: 'GAP', cssVar: '--spacing-xl' },
{ name: 'spacing/2xl', value: 48, scope: 'GAP', cssVar: '--spacing-2xl' },
];
const radiusTokens = [
{ name: 'radius/none', value: 0, scope: 'CORNER_RADIUS', cssVar: '--radius-none' },
{ name: 'radius/sm', value: 4, scope: 'CORNER_RADIUS', cssVar: '--radius-sm' },
{ name: 'radius/md', value: 8, scope: 'CORNER_RADIUS', cssVar: '--radius-md' },
{ name: 'radius/lg', value: 16, scope: 'CORNER_RADIUS', cssVar: '--radius-lg' },
{ name: 'radius/full', value: 9999, scope: 'CORNER_RADIUS', cssVar: '--radius-full' },
];
const created = [];
for (const { name, value, scope, cssVar } of [...spacingTokens, ...radiusTokens]) {
const v = figma.variables.createVariable(name, spacingColl, 'FLOAT');
v.setValueForMode(valueMode, value);
v.scopes = [scope];
v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
created.push({ name, value, id: v.id });
}
return { created, count: created.length };
```
### Creating STRING Variables (Font Family, Font Style)
```javascript
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const typoPrimColl = collections.find(c => c.name === 'Typography Primitives');
if (!typoPrimColl) throw new Error("Typography Primitives collection not found");
const valueMode = typoPrimColl.modes[0].modeId;
const fontTokens = [
{ name: 'family/sans', value: 'Inter', scope: 'FONT_FAMILY', cssVar: '--font-family-sans' },
{ name: 'family/mono', value: 'Roboto Mono', scope: 'FONT_FAMILY', cssVar: '--font-family-mono' },
// Font style strings — these are the Figma fontName.style values:
{ name: 'weight/regular', value: 'Regular', scope: 'FONT_STYLE', cssVar: '--font-weight-regular' },
{ name: 'weight/medium', value: 'Medium', scope: 'FONT_STYLE', cssVar: '--font-weight-medium' },
{ name: 'weight/semibold', value: 'Semi Bold', scope: 'FONT_STYLE', cssVar: '--font-weight-semibold' },
{ name: 'weight/bold', value: 'Bold', scope: 'FONT_STYLE', cssVar: '--font-weight-bold' },
];
const created = [];
for (const { name, value, scope, cssVar } of fontTokens) {
const v = figma.variables.createVariable(name, typoPrimColl, 'STRING');
v.setValueForMode(valueMode, value);
v.scopes = [scope];
v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
created.push({ name, value, id: v.id });
}
return { created, count: created.length };
```
### Creating BOOLEAN Variables
BOOLEAN variables have no scopes (scopes are not supported for BOOLEAN type).
```javascript
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const coll = collections.find(c => c.name === 'Tokens');
if (!coll) throw new Error("Collection not found");
const valueMode = coll.modes[0].modeId;
const v = figma.variables.createVariable('feature-flags/show-beta-badge', coll, 'BOOLEAN');
v.setValueForMode(valueMode, false);
// No scopes — BOOLEAN does not support scopes
return { id: v.id, name: v.name };
```
---
## 4. Variable Aliasing (VARIABLE_ALIAS) — Primitive → Semantic Chain
Semantic tokens reference primitives via `VARIABLE_ALIAS`. This is the core pattern that makes light/dark theming work.
**Architecture:**
```
Color Primitives collection (1 mode: Value)
blue/500 = #3B82F6 ← raw value
Color collection (2 modes: Light, Dark)
color/bg/accent/default:
Light → VARIABLE_ALIAS → Primitives/blue/500
Dark → VARIABLE_ALIAS → Primitives/blue/300
```
### Complete Semantic Alias Creation Script (SDS-style)
```javascript
function hexToRgb(hex) {
const c = hex.replace('#', '');
return { r: parseInt(c.slice(0,2),16)/255, g: parseInt(c.slice(2,4),16)/255, b: parseInt(c.slice(4,6),16)/255 };
}
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const primColl = collections.find(c => c.name === 'Primitives');
const colorColl = collections.find(c => c.name === 'Color');
if (!primColl || !colorColl) throw new Error("Collections not found — run primitive/color collection creation first");
const primValueMode = primColl.modes[0].modeId;
const lightModeId = colorColl.modes.find(m => m.name === 'Light').modeId;
const darkModeId = colorColl.modes.find(m => m.name === 'Dark').modeId;
// Load all primitive variables for lookup
const allVars = await figma.variables.getLocalVariablesAsync();
const primsByName = {};
for (const v of allVars) {
if (v.variableCollectionId === primColl.id) {
primsByName[v.name] = v;
}
}
function getPrim(name) {
const v = primsByName[name];
if (!v) throw new Error(`Primitive not found: ${name}`);
return v;
}
// Define semantic → [lightPrimitiveName, darkPrimitiveName]
// Following the SDS pattern: Background/{Intent}/{Emphasis}
const semanticColors = [
// Background
{ name: 'color/bg/default/default', lightPrim: 'white/1000', darkPrim: 'gray/900',
cssVar: '--color-bg-default-default', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
{ name: 'color/bg/default/secondary', lightPrim: 'gray/100', darkPrim: 'gray/800',
cssVar: '--color-bg-default-secondary', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
{ name: 'color/bg/brand/default', lightPrim: 'blue/600', darkPrim: 'blue/300',
cssVar: '--color-bg-brand-default', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
// Text
{ name: 'color/text/default/default', lightPrim: 'gray/900', darkPrim: 'white/1000',
cssVar: '--color-text-default-default', scopes: ['TEXT_FILL'] },
{ name: 'color/text/default/secondary', lightPrim: 'gray/500', darkPrim: 'gray/400',
cssVar: '--color-text-default-secondary', scopes: ['TEXT_FILL'] },
{ name: 'color/text/brand/default', lightPrim: 'blue/700', darkPrim: 'blue/200',
cssVar: '--color-text-brand-default', scopes: ['TEXT_FILL'] },
// Border
{ name: 'color/border/default/default', lightPrim: 'gray/300', darkPrim: 'gray/600',
cssVar: '--color-border-default-default', scopes: ['STROKE_COLOR'] },
{ name: 'color/border/brand/default', lightPrim: 'blue/500', darkPrim: 'blue/400',
cssVar: '--color-border-brand-default', scopes: ['STROKE_COLOR'] },
];
const created = [];
for (const { name, lightPrim, darkPrim, cssVar, scopes } of semanticColors) {
const v = figma.variables.createVariable(name, colorColl, 'COLOR');
// Alias to primitive in Light mode
v.setValueForMode(lightModeId, figma.variables.createVariableAlias(getPrim(lightPrim)));
// Alias to primitive in Dark mode
v.setValueForMode(darkModeId, figma.variables.createVariableAlias(getPrim(darkPrim)));
// Set scopes (semantic layer — these ARE shown in pickers)
v.scopes = scopes;
// Code syntax
v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
created.push({ name, id: v.id });
}
return { created, count: created.length };
```
**Key API points:**
- `figma.variables.createVariableAlias(variable)` — takes a Variable object, returns `{type:'VARIABLE_ALIAS', id: variable.id}`
- The aliased variable MUST have the same `resolvedType` as the semantic variable
- Never duplicate raw values in the semantic layer — always alias
---
## 5. Variable Scopes — Complete Reference Table
| Semantic Role | Recommended Scopes | Variable Type |
|---|---|---|
| Primitive colors (raw) | `[]` — empty, hidden from all pickers | COLOR |
| Semi-transparent overlay primitives | `["EFFECT_COLOR"]` | COLOR |
| Background fills (frame, shape) | `["FRAME_FILL", "SHAPE_FILL"]` | COLOR |
| Text color | `["TEXT_FILL"]` | COLOR |
| Icon / shape fill | `["SHAPE_FILL", "STROKE_COLOR"]` | COLOR |
| Border / stroke color | `["STROKE_COLOR"]` | COLOR |
| Background + border combined | `["FRAME_FILL", "SHAPE_FILL", "STROKE_COLOR"]` | COLOR |
| Shadow color | `["EFFECT_COLOR"]` | COLOR |
| Spacing / gap between items | `["GAP"]` | FLOAT |
| Padding (if separate from gap) | `["GAP"]` | FLOAT |
| Corner radius | `["CORNER_RADIUS"]` | FLOAT |
| Width / height dimensions | `["WIDTH_HEIGHT"]` | FLOAT |
| Font size | `["FONT_SIZE"]` | FLOAT |
| Line height | `["LINE_HEIGHT"]` | FLOAT |
| Letter spacing | `["LETTER_SPACING"]` | FLOAT |
| Font weight (numeric) | `["FONT_WEIGHT"]` | FLOAT |
| Stroke width | `["STROKE_FLOAT"]` | FLOAT |
| Effect blur radius | `["EFFECT_FLOAT"]` | FLOAT |
| Opacity | `["OPACITY"]` | FLOAT |
| Font family | `["FONT_FAMILY"]` | STRING |
| Font style (font-specific name, e.g. `"Regular"` — varies per font) | `["FONT_STYLE"]` | STRING |
| Boolean flags | *(scopes not supported)* | BOOLEAN |
**Never use `ALL_SCOPES`** on any variable. It pollutes every picker with irrelevant tokens. The Simple Design System (SDS), the gold standard, uses targeted scopes on every variable.
**`ALL_FILLS` note:** `ALL_FILLS` is exclusive among fill scopes — it covers `FRAME_FILL`, `SHAPE_FILL`, and `TEXT_FILL` together. If set, you cannot also add individual fill scopes. Prefer specifying individual scopes for precision.
### Batch Scope-Setting (After Variables are Created)
If you created variables without scopes and need to set them in batch:
```javascript
const allVars = await figma.variables.getLocalVariablesAsync();
// Scope mapping: partial name match → scopes
const scopeRules = [
{ match: 'color/bg/', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
{ match: 'color/text/', scopes: ['TEXT_FILL'] },
{ match: 'color/icon/', scopes: ['SHAPE_FILL', 'STROKE_COLOR'] },
{ match: 'color/border/', scopes: ['STROKE_COLOR'] },
{ match: 'spacing/', scopes: ['GAP'] },
{ match: 'radius/', scopes: ['CORNER_RADIUS'] },
{ match: 'blue/', scopes: [] }, // primitives — hide
{ match: 'gray/', scopes: [] },
{ match: 'white/', scopes: [] },
{ match: 'black/', scopes: [] },
];
const updated = [];
for (const v of allVars) {
if (v.remote) continue; // skip library variables
for (const rule of scopeRules) {
if (v.name.startsWith(rule.match)) {
v.scopes = rule.scopes;
updated.push({ name: v.name, scopes: rule.scopes });
break;
}
}
}
return { updated, count: updated.length };
```
---
## 6. Code Syntax — WEB/ANDROID/iOS
Every variable must have code syntax set. This is what powers the developer handoff experience:
**What code syntax does:** When a developer inspects any element in Figma Dev Mode that has a variable-bound property (fill, padding, radius, etc.), the code snippet shown uses the variable's code syntax name — not the Figma variable name. For example, a button's background fill bound to `color/bg/primary` will show `background: var(--color-bg-primary)` in the CSS snippet, not `color/bg/primary`. Without code syntax set, Dev Mode shows raw hex values or nothing useful.
You can set up to **3 syntaxes per variable** — one per platform (Web, iOS, Android). Set all three if the codebase targets multiple platforms; set only WEB if it's a web-only project.
```javascript
// WEB: MUST include the var() wrapper — this is the full CSS function syntax
variable.setVariableCodeSyntax('WEB', 'var(--color-bg-primary)');
// ^^^^ ^
// var() wrapper is REQUIRED
// ANDROID: Kotlin property name — camelCase, no wrapper
variable.setVariableCodeSyntax('ANDROID', 'colorBgPrimary');
// iOS: Swift property — dot-notation, no wrapper
variable.setVariableCodeSyntax('iOS', 'Color.bgPrimary');
```
> **CRITICAL — WEB code syntax MUST use the `var()` wrapper.** Setting just `--color-bg-primary` (without `var()`) will cause Dev Mode to show raw hex values instead of the CSS variable reference. Always use the full `var(--name)` form. ANDROID and iOS do NOT use a wrapper.
**Platform derivation rules from the CSS variable name:**
| Platform | Pattern | Example |
|---|---|---|
| WEB | **`var(--{css-var-name})`** — `var()` wrapper required | `var(--sds-color-bg-primary)` |
| ANDROID | camelCase, no wrapper, strip `--` prefix | `sdsColorBgPrimary` |
| iOS | PascalCase after `.`, no wrapper, strip `--` prefix | `Color.SdsColorBgPrimary` or `Color.bgPrimary` |
**Always use the actual CSS variable name from the codebase** — do not derive it from the Figma variable name. If the code uses `--sds-color-background-brand-default`, that exact string is the WEB code syntax (minus the `var()` wrapper that you add).
### Batch Code Syntax Setting
```javascript
const allVars = await figma.variables.getLocalVariablesAsync();
const updated = [];
for (const v of allVars) {
if (v.remote) continue;
// If code syntax already set, skip
if (v.codeSyntax['WEB']) continue;
// FALLBACK: derive from Figma name: color/bg/primary → var(--color-bg-primary)
// PREFERRED: pass in a cssVarMap built from actual codebase CSS variable names
// e.g. cssVarMap = { 'color/bg/primary': '--color-bg-primary', ... }
const cssName = cssVarMap?.[v.name]
?? v.name.replace(/\//g, '-').replace(/\s/g, '-').toLowerCase();
v.setVariableCodeSyntax('WEB', `var(--${cssName})`);
updated.push({ name: v.name, web: `var(--${cssName})` });
}
return { updated, count: updated.length };
```
Note: derived names are a fallback only. Always prefer overriding with actual CSS variable names from the codebase when they are known.
---
## 7. Effect Styles (Shadows) and Text Styles
Shadows and composite typography cannot be variables — they are Styles.
### Creating Effect Styles (Shadows)
Reference from SDS (15 effect styles) and the SDS shadow pattern `Shadow/{Level}`:
```javascript
// Shadow definitions — CSS equivalent in comments
// CSS: 0 1px 2px rgba(0,0,0,0.05)
const shadows = [
{
name: 'Shadow/Subtle',
effects: [{
type: 'DROP_SHADOW',
color: { r: 0, g: 0, b: 0, a: 0.05 },
offset: { x: 0, y: 1 },
radius: 2,
spread: 0,
visible: true,
blendMode: 'NORMAL'
}]
},
{
// CSS: 0 4px 6px -1px rgba(0,0,0,0.10), 0 2px 4px -1px rgba(0,0,0,0.06)
name: 'Shadow/Medium',
effects: [
{
type: 'DROP_SHADOW',
color: { r: 0, g: 0, b: 0, a: 0.10 },
offset: { x: 0, y: 4 },
radius: 6,
spread: -1,
visible: true,
blendMode: 'NORMAL'
},
{
type: 'DROP_SHADOW',
color: { r: 0, g: 0, b: 0, a: 0.06 },
offset: { x: 0, y: 2 },
radius: 4,
spread: -1,
visible: true,
blendMode: 'NORMAL'
}
]
},
{
// CSS: 0 10px 15px -3px rgba(0,0,0,0.10), 0 4px 6px -2px rgba(0,0,0,0.05)
name: 'Shadow/Strong',
effects: [
{
type: 'DROP_SHADOW',
color: { r: 0, g: 0, b: 0, a: 0.10 },
offset: { x: 0, y: 10 },
radius: 15,
spread: -3,
visible: true,
blendMode: 'NORMAL'
},
{
type: 'DROP_SHADOW',
color: { r: 0, g: 0, b: 0, a: 0.05 },
offset: { x: 0, y: 4 },
radius: 6,
spread: -2,
visible: true,
blendMode: 'NORMAL'
}
]
}
];
// M3-style dual shadow (umbra + penumbra pattern):
const m3Shadows = [
{
name: 'Elevation/1',
effects: [
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:2, spread:0, visible:true, blendMode:'NORMAL' },
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:1}, radius:3, spread:1, visible:true, blendMode:'NORMAL' }
]
},
{
name: 'Elevation/2',
effects: [
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:2, spread:0, visible:true, blendMode:'NORMAL' },
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:2}, radius:6, spread:2, visible:true, blendMode:'NORMAL' }
]
},
{
name: 'Elevation/3',
effects: [
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:3, spread:0, visible:true, blendMode:'NORMAL' },
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:4}, radius:8, spread:3, visible:true, blendMode:'NORMAL' }
]
}
];
const created = [];
for (const { name, effects } of shadows) {
const style = figma.createEffectStyle();
style.name = name;
style.effects = effects;
created.push({ name, id: style.id });
}
return { created, count: created.length };
```
### Creating Text Styles
Fonts must be loaded before creating text styles.
```javascript
// Define text styles — based on SDS typography hierarchy
const textStyles = [
// Display / Hero
{ name: 'Display/Hero', family: 'Inter', style: 'Bold', size: 72, lineHeight: 80, letterSpacing: -1.5 },
// Headings
{ name: 'Heading/H1', family: 'Inter', style: 'Bold', size: 48, lineHeight: 56, letterSpacing: -1.0 },
{ name: 'Heading/H2', family: 'Inter', style: 'Bold', size: 40, lineHeight: 48, letterSpacing: -0.5 },
{ name: 'Heading/H3', family: 'Inter', style: 'Semi Bold', size: 32, lineHeight: 40, letterSpacing: 0 },
{ name: 'Heading/H4', family: 'Inter', style: 'Semi Bold', size: 24, lineHeight: 32, letterSpacing: 0 },
// Body
{ name: 'Body/Large', family: 'Inter', style: 'Regular', size: 18, lineHeight: 28, letterSpacing: 0 },
{ name: 'Body/Medium', family: 'Inter', style: 'Regular', size: 16, lineHeight: 24, letterSpacing: 0 },
{ name: 'Body/Small', family: 'Inter', style: 'Regular', size: 14, lineHeight: 20, letterSpacing: 0 },
// Label
{ name: 'Label/Large', family: 'Inter', style: 'Medium', size: 14, lineHeight: 20, letterSpacing: 0.1 },
{ name: 'Label/Medium', family: 'Inter', style: 'Medium', size: 12, lineHeight: 16, letterSpacing: 0.5 },
{ name: 'Label/Small', family: 'Inter', style: 'Medium', size: 11, lineHeight: 16, letterSpacing: 0.5 },
// Code
{ name: 'Code/Base', family: 'Roboto Mono', style: 'Regular', size: 14, lineHeight: 20, letterSpacing: 0 },
];
// Verify fonts are available, then load them
const allFonts = await figma.listAvailableFontsAsync();
const availableFontNames = new Set(allFonts.map(f => JSON.stringify(f.fontName)));
const fontSet = new Set(textStyles.map(s => JSON.stringify({ family: s.family, style: s.style })));
for (const f of fontSet) {
if (!availableFontNames.has(f)) {
const parsed = JSON.parse(f);
const familyFonts = allFonts.filter(af => af.fontName.family === parsed.family);
throw new Error(`Font "${parsed.family} ${parsed.style}" not available. Available styles: ${familyFonts.map(af => af.fontName.style).join(', ') || 'none'}`);
}
}
await Promise.all([...fontSet].map(f => figma.loadFontAsync(JSON.parse(f))));
const created = [];
for (const { name, family, style, size, lineHeight, letterSpacing } of textStyles) {
const ts = figma.createTextStyle();
ts.name = name;
ts.fontName = { family, style };
ts.fontSize = size;
ts.lineHeight = { value: lineHeight, unit: 'PIXELS' };
ts.letterSpacing = { value: letterSpacing, unit: 'PIXELS' };
created.push({ name, id: ts.id });
}
return { created, count: created.length };
```
---
## 8. Idempotency — Check-Before-Create Pattern
Every creation script should check whether the entity already exists before creating it. This prevents duplicates when a script is re-run after partial failure.
### Check-Before-Create for Collections
```javascript
// Check if already exists
const existing = await figma.variables.getLocalVariableCollectionsAsync();
let primColl = existing.find(c => c.name === 'Primitives');
if (primColl) {
return { status: 'already_exists', collectionId: primColl.id, name: primColl.name };
}
// Create only if not found
primColl = figma.variables.createVariableCollection("Primitives");
primColl.renameMode(primColl.modes[0].modeId, "Value");
return { status: 'created', collectionId: primColl.id };
```
### Check-Before-Create for Variables
```javascript
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const primColl = collections.find(c => c.name === 'Primitives');
if (!primColl) throw new Error('Primitives collection not found');
// Variable names are unique within this deterministic collection.
const allVars = await figma.variables.getLocalVariablesAsync();
const existing = allVars.find(v =>
v.variableCollectionId === primColl.id && v.name === 'blue/500'
);
if (existing) {
return { status: 'already_exists', id: existing.id, name: existing.name };
}
// ... create the variable ...
return { status: 'created' };
```
**Cleanup by exact state-ledger ID:**
```javascript
const VARIABLE_IDS = ['VariableID:1:8', 'VariableID:1:9'];
const removed = [];
for (const id of VARIABLE_IDS) {
const variable = await figma.variables.getVariableByIdAsync(id);
if (!variable) continue;
removed.push(variable.id);
variable.remove();
}
return { removed, count: removed.length };
```
**Never clean up by a name or prefix.** It can match data not created by the current workflow.
---
## 9. Validation — Verify Counts, Aliases, and Scopes
Run the applicable scripts after creating or updating the selected tokens. Use the results to stop a token-only workflow or to continue to an in-scope dependent asset.
### Verify Collection and Variable Counts
```javascript
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const allVars = await figma.variables.getLocalVariablesAsync();
const summary = collections.map(c => {
const vars = allVars.filter(v => v.variableCollectionId === c.id);
return {
name: c.name,
id: c.id,
modes: c.modes.map(m => m.name),
variableCount: vars.length,
missingScopes: vars.filter(v => v.scopes.length === 0 && v.resolvedType !== 'BOOLEAN').length,
missingCodeSyntax: vars.filter(v => !v.codeSyntax['WEB'] && !v.remote).length,
sampleVariables: vars.slice(0, 3).map(v => v.name)
};
});
return {
collectionCount: collections.length,
totalVariables: allVars.length,
collections: summary
};
```
Interpret: `missingScopes > 0` (for non-primitives and non-BOOLEANs) → scope-setting failed, re-run scope script. `missingCodeSyntax > 0` → code syntax not set, run batch code syntax script.
Note: primitives correctly have `scopes = []` (empty, hidden). `missingScopes` above counts non-BOOLEAN variables with empty scopes — review the list to confirm they are all primitives.
### Verify Aliases Resolve
```javascript
const allVars = await figma.variables.getLocalVariablesAsync();
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const brokenAliases = [];
const aliasedVars = [];
for (const v of allVars) {
if (v.remote) continue;
const coll = collections.find(c => c.id === v.variableCollectionId);
if (!coll) continue;
for (const [modeId, val] of Object.entries(v.valuesByMode)) {
if (val && typeof val === 'object' && val.type === 'VARIABLE_ALIAS') {
aliasedVars.push({ name: v.name, aliasTargetId: val.id });
// Verify the target exists
const target = allVars.find(t => t.id === val.id);
if (!target) {
brokenAliases.push({ variable: v.name, modeId, missingTargetId: val.id });
}
}
}
}
return {
totalAliased: aliasedVars.length,
brokenAliases,
brokenCount: brokenAliases.length,
status: brokenAliases.length === 0 ? 'all_aliases_resolve' : 'BROKEN_ALIASES_FOUND'
};
```
Interpret: `brokenCount > 0` means a semantic variable references a primitive that was deleted or not yet created. Create the missing primitives, then re-run alias creation for the affected semantic variables.
### Verify Style Counts
```javascript
const [textStyles, effectStyles] = await Promise.all([
figma.getLocalTextStylesAsync(),
figma.getLocalEffectStylesAsync()
]);
return {
textStyles: textStyles.map(s => ({ name: s.name, fontSize: s.fontSize, fontFamily: s.fontName.family })),
effectStyles: effectStyles.map(s => ({ name: s.name, effectCount: s.effects.length })),
counts: { text: textStyles.length, effect: effectStyles.length }
};
```
### Token Exit Criteria Checklist
Before declaring token work complete or proceeding to an in-scope dependent asset, verify all of the following:
- Every planned collection exists with the correct number of modes
- Primitive variables: `scopes = []`, code syntax set
- Semantic variables: targeted scopes set, code syntax set, aliases pointing to primitives (not raw values)
- All broken alias count = 0
- All planned text styles exist with correct font family/size/weight
- All planned effect styles exist with correct shadow values
- No variable has `ALL_SCOPES` unless explicitly approved by the user
scripts/bindVariablesToComponent.js
/**
* bindVariablesToComponent
*
* Binds design token variables to the visual properties of a component node.
* Supports fills, strokes, all padding directions, item spacing, and corner radius.
* Only binds properties for which a variable ID is provided in `bindings`.
*
* This function should be called on each variant individually within a component
* set, OR on the component set itself for properties shared by all variants.
*
* @param {ComponentNode | FrameNode | RectangleNode} component
* The Figma node to mutate. Usually a ComponentNode or one of its children.
* @param {{
* fills?: string,
* strokes?: string,
* paddingTop?: string,
* paddingBottom?: string,
* paddingLeft?: string,
* paddingRight?: string,
* itemSpacing?: string,
* cornerRadius?: string
* }} bindings
* Each key is a visual property name; each value is a Figma Variable ID
* (e.g. "VariableID:123:456"). Omit a key to skip binding that property.
* @returns {Promise<{ mutatedNodeIds: string[] }>}
* List of node IDs that were mutated (for audit/validation purposes).
*/
async function bindVariablesToComponent(component, bindings) {
const mutatedNodeIds = []
if (!component) {
return { mutatedNodeIds }
}
// Batch every getVariableByIdAsync call upfront in a single Promise.all rather
// than awaiting per-property — the lookups are independent and IPC-bound.
const floatBindings = [
['paddingTop', 'paddingTop'],
['paddingBottom', 'paddingBottom'],
['paddingLeft', 'paddingLeft'],
['paddingRight', 'paddingRight'],
['itemSpacing', 'itemSpacing'],
['cornerRadius', 'cornerRadius'],
]
const requestedIds = []
if (bindings.fills) requestedIds.push(['fills', bindings.fills])
if (bindings.strokes) requestedIds.push(['strokes', bindings.strokes])
for (const [bindingKey] of floatBindings) {
if (bindings[bindingKey]) requestedIds.push([bindingKey, bindings[bindingKey]])
}
const resolved = await Promise.all(
requestedIds.map(([, id]) => figma.variables.getVariableByIdAsync(id)),
)
const varByKey = {}
for (let i = 0; i < requestedIds.length; i++) {
varByKey[requestedIds[i][0]] = resolved[i]
}
const markMutated = () => {
if (!mutatedNodeIds.includes(component.id)) {
mutatedNodeIds.push(component.id)
}
}
// --- Fills ---
const fillVar = varByKey.fills
if (fillVar) {
const existingFills = component.fills
if (Array.isArray(existingFills) && existingFills.length > 0) {
// Bind the color of the first fill to the variable
const boundFill = figma.variables.setBoundVariableForPaint(existingFills[0], 'color', fillVar)
component.fills = [boundFill, ...existingFills.slice(1)]
} else {
// No existing fill — create a solid fill bound to the variable
const boundFill = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } },
'color',
fillVar,
)
component.fills = [boundFill]
}
markMutated()
}
// --- Strokes ---
const strokeVar = varByKey.strokes
if (strokeVar) {
const existingStrokes = component.strokes
if (Array.isArray(existingStrokes) && existingStrokes.length > 0) {
const boundStroke = figma.variables.setBoundVariableForPaint(
existingStrokes[0],
'color',
strokeVar,
)
component.strokes = [boundStroke, ...existingStrokes.slice(1)]
} else {
const boundStroke = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } },
'color',
strokeVar,
)
component.strokes = [boundStroke]
}
markMutated()
}
// --- Spacing properties (FLOAT variables bound via setBoundVariable) ---
for (const [bindingKey, figmaProp] of floatBindings) {
const variable = varByKey[bindingKey]
if (variable) {
component.setBoundVariable(figmaProp, variable)
markMutated()
}
}
return { mutatedNodeIds }
}
scripts/cleanupOrphans.js
/**
* Removes entities identified by the exact IDs recorded in the state ledger.
*
* @param {{
* nodeIds?: string[],
* variableIds?: string[],
* collectionIds?: string[]
* }} ids
* @returns {Promise<{
* removedCount: number,
* removedIds: string[],
* skippedIds: string[]
* }>}
*/
async function cleanupOrphans({ nodeIds = [], variableIds = [], collectionIds = [] } = {}) {
const requestedIds = [...nodeIds, ...variableIds, ...collectionIds]
if (requestedIds.length === 0) {
throw new Error('cleanupOrphans: pass IDs from the state ledger.')
}
const removedIds = []
const skippedIds = []
const nodes = []
for (const id of new Set(nodeIds)) {
const node = await figma.getNodeByIdAsync(id)
if (!node) {
skippedIds.push(id)
continue
}
nodes.push(node)
}
for (const node of nodes.sort((a, b) => getDepth(b) - getDepth(a))) {
if (node.type === 'DOCUMENT') {
skippedIds.push(node.id)
continue
}
if (node.type === 'PAGE') {
if (figma.root.children.length <= 1) {
skippedIds.push(node.id)
continue
}
if (figma.currentPage.id === node.id) {
const fallbackPage = figma.root.children.find((page) => page.id !== node.id)
if (!fallbackPage) {
skippedIds.push(node.id)
continue
}
await figma.setCurrentPageAsync(fallbackPage)
}
}
removedIds.push(node.id)
node.remove()
}
for (const id of new Set(variableIds)) {
const variable = await figma.variables.getVariableByIdAsync(id)
if (!variable) {
skippedIds.push(id)
continue
}
removedIds.push(variable.id)
variable.remove()
}
for (const id of new Set(collectionIds)) {
const collection = await figma.variables.getVariableCollectionByIdAsync(id)
if (!collection) {
skippedIds.push(id)
continue
}
removedIds.push(collection.id)
collection.remove()
}
return {
removedCount: removedIds.length,
removedIds,
skippedIds,
}
}
function getDepth(node) {
let depth = 0
let current = node
while (current.parent) {
depth++
current = current.parent
}
return depth
}
scripts/createComponentWithVariants.js
/**
* createComponentWithVariants
*
* Creates a component set by generating all combinations of `variantAxes`,
* building one Figma component per combination, then calling
* `figma.combineAsVariants` to produce the component set. After combining,
* the variants are repositioned into a grid so they don't all stack at (0, 0).
*
* @param {{
* name: string,
* description?: string,
* variantAxes: Record<string, string[]>,
* baseProps: {
* width: number,
* height: number,
* fills?: Paint[],
* padding?: {top?: number, bottom?: number, left?: number, right?: number},
* radius?: number,
* layoutMode?: 'HORIZONTAL' | 'VERTICAL' | 'NONE',
* itemSpacing?: number
* },
* page: PageNode
* }} config
* - `name`: Component set name (e.g. "Button").
* - `description`: Optional human-readable purpose and usage guidance.
* - `variantAxes`: Each key is a variant property name; each value is an array of
* allowed values. All combinations are generated (Cartesian product).
* Example: { Size: ['Small', 'Medium', 'Large'], Style: ['Primary', 'Ghost'] }
* produces 6 variants.
* - `baseProps`: Visual properties applied to every variant.
* - `page`: The PageNode to create components on (must be set as current page by caller).
* @returns {Promise<{
* componentSet: ComponentSetNode,
* variants: ComponentNode[]
* }>}
*/
async function createComponentWithVariants(config) {
const { name, variantAxes, baseProps, page } = config
// Ensure we are on the correct page
await figma.setCurrentPageAsync(page)
// Compute Cartesian product of variant axes
const axisNames = Object.keys(variantAxes)
const axisValues = axisNames.map((k) => variantAxes[k])
const combinations = cartesianProduct(axisValues)
// Build one component per combination
const components = []
for (const combo of combinations) {
const comp = figma.createComponent()
// Name: "Property=Value, Property=Value, ..."
comp.name = axisNames.map((ax, i) => `${ax}=${combo[i]}`).join(', ')
// Base geometry
comp.resize(baseProps.width, baseProps.height)
// Fills
if (baseProps.fills !== undefined) {
comp.fills = baseProps.fills
} else {
comp.fills = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }]
}
// Corner radius
if (baseProps.radius !== undefined) {
comp.cornerRadius = baseProps.radius
}
// Auto-layout
if (baseProps.layoutMode && baseProps.layoutMode !== 'NONE') {
comp.layoutMode = baseProps.layoutMode
comp.primaryAxisAlignItems = 'CENTER'
comp.counterAxisAlignItems = 'CENTER'
if (baseProps.itemSpacing !== undefined) {
comp.itemSpacing = baseProps.itemSpacing
}
}
// Padding
if (baseProps.padding) {
comp.paddingTop = baseProps.padding.top ?? 0
comp.paddingBottom = baseProps.padding.bottom ?? 0
comp.paddingLeft = baseProps.padding.left ?? 0
comp.paddingRight = baseProps.padding.right ?? 0
}
page.appendChild(comp)
components.push(comp)
}
// Combine into a component set
const componentSet = figma.combineAsVariants(components, page)
componentSet.name = name
if (config.description) {
componentSet.description = config.description
}
// Grid layout — variants stack at (0, 0) after combineAsVariants; reposition them.
const GRID_GAP = 16
const cols = Math.max(1, axisValues[axisValues.length - 1]?.length ?? 1)
const variantWidth = baseProps.width
const variantHeight = baseProps.height
componentSet.children.forEach((variant, idx) => {
const col = idx % cols
const row = Math.floor(idx / cols)
variant.x = col * (variantWidth + GRID_GAP)
variant.y = row * (variantHeight + GRID_GAP)
})
// Resize component set to wrap its children with padding
const totalCols = Math.min(cols, combinations.length)
const totalRows = Math.ceil(combinations.length / cols)
const PADDING = 40
componentSet.resize(
totalCols * variantWidth + (totalCols - 1) * GRID_GAP + PADDING * 2,
totalRows * variantHeight + (totalRows - 1) * GRID_GAP + PADDING * 2,
)
// Position component set at a safe canvas location
componentSet.x = 480
componentSet.y = 80
return { componentSet, variants: componentSet.children }
}
/**
* Computes the Cartesian product of multiple arrays.
* cartesianProduct([[A, B], [1, 2]]) → [[A,1], [A,2], [B,1], [B,2]]
*
* @param {Array<string[]>} arrays
* @returns {string[][]}
*/
function cartesianProduct(arrays) {
return arrays.reduce(
(acc, curr) => acc.flatMap((combo) => curr.map((val) => [...combo, val])),
[[]],
)
}
scripts/createDocumentationPage.js
/**
* createDocumentationPage
*
* Creates a new Figma page with a standardized documentation layout: a page
* title, optional description, and an ordered list of sections each built by
* a caller-supplied `contentFn`. The content function receives the section
* frame and may append any nodes to it.
*
* This function is used for standalone documentation pages (e.g. a Foundations
* page, a Getting Started page, or a component page with documentation).
* It does not handle component sets — those live on separate pages created by
* createComponentWithVariants.
*
* @param {string} pageName - The Figma page name (e.g. "Foundations", "Getting Started").
* @param {{
* title: string,
* description?: string,
* sections: Array<{
* name: string,
* contentFn: (sectionFrame: FrameNode) => Promise<void>
* }>
* }} config
* - `title`: Large heading displayed at the top of the page.
* - `description`: Optional subtitle displayed below the heading.
* - `sections`: Ordered list of sections. Each section gets its own frame
* with a heading and is passed to `contentFn` for population.
* @returns {Promise<{
* page: PageNode,
* titleNode: TextNode,
* frameIds: string[]
* }>}
* `frameIds` is an ordered list of IDs for the root frame and each section frame.
*/
async function createDocumentationPage(pageName, config) {
// Verify required fonts are available before loading
const allFonts = await figma.listAvailableFontsAsync()
const requiredStyles = ['Bold', 'Regular', 'Medium']
for (const style of requiredStyles) {
const found = allFonts.some((f) => f.fontName.family === 'Inter' && f.fontName.style === style)
if (!found) {
const interFonts = allFonts.filter((f) => f.fontName.family === 'Inter')
throw new Error(
`Font "Inter ${style}" not available. Available Inter styles: ${interFonts.map((f) => f.fontName.style).join(', ') || 'none'}`,
)
}
}
await Promise.all([
figma.loadFontAsync({ family: 'Inter', style: 'Bold' }),
figma.loadFontAsync({ family: 'Inter', style: 'Regular' }),
figma.loadFontAsync({ family: 'Inter', style: 'Medium' }),
])
// Create and activate the page
const page = figma.createPage()
page.name = pageName
await figma.setCurrentPageAsync(page)
const frameIds = []
// Root scroll container — 1440px wide, auto-height
const root = figma.createAutoLayout('VERTICAL')
root.name = pageName
root.primaryAxisAlignItems = 'MIN'
root.counterAxisAlignItems = 'MIN'
root.itemSpacing = 80
root.paddingTop = 80
root.paddingBottom = 120
root.paddingLeft = 80
root.paddingRight = 80
root.resize(1440, 1)
root.layoutSizingHorizontal = 'FIXED'
root.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]
root.x = 0
root.y = 0
page.appendChild(root)
frameIds.push(root.id)
// Page header: title + optional description
const header = figma.createAutoLayout('VERTICAL')
header.name = 'Header'
header.itemSpacing = 12
header.fills = []
root.appendChild(header)
header.layoutSizingHorizontal = 'FILL'
const titleNode = figma.createText()
titleNode.fontName = { family: 'Inter', style: 'Bold' }
titleNode.characters = config.title
titleNode.fontSize = 40
titleNode.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }]
header.appendChild(titleNode)
titleNode.layoutSizingHorizontal = 'FILL'
if (config.description) {
const descNode = figma.createText()
descNode.fontName = { family: 'Inter', style: 'Regular' }
descNode.characters = config.description
descNode.fontSize = 16
descNode.lineHeight = { value: 24, unit: 'PIXELS' }
descNode.fills = [{ type: 'SOLID', color: { r: 0.4, g: 0.4, b: 0.4 } }]
header.appendChild(descNode)
descNode.layoutSizingHorizontal = 'FILL'
}
// Sections
for (const section of config.sections) {
const sectionFrame = figma.createAutoLayout('VERTICAL')
sectionFrame.name = `Section/${section.name}`
sectionFrame.itemSpacing = 20
sectionFrame.fills = []
root.appendChild(sectionFrame)
sectionFrame.layoutSizingHorizontal = 'FILL'
// Section heading
const sectionHeading = figma.createText()
sectionHeading.fontName = { family: 'Inter', style: 'Bold' }
sectionHeading.characters = section.name
sectionHeading.fontSize = 24
sectionHeading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }]
sectionFrame.appendChild(sectionHeading)
sectionHeading.layoutSizingHorizontal = 'FILL'
// Invoke the caller's content function to populate the section
await section.contentFn(sectionFrame)
frameIds.push(sectionFrame.id)
}
return { page, titleNode, frameIds }
}
scripts/createSemanticTokens.js
/**
* createSemanticTokens
*
* Creates a batch of Figma variables in the given collection, one per entry in
* `tokenMap`. Supports raw values, variable alias references, code syntax, and
* scopes. Returns a map of token name → Variable for use in subsequent steps.
*
* @param {VariableCollection} collection - The target variable collection.
* @param {Record<string, string>} modeIds - Map of {modeName: modeId} from createVariableCollection.
* @param {Array<{
* name: string,
* type: 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN' | 'TIMING' | 'EASING',
* values: Record<string, string | number | boolean | MotionEasing | {type: 'VARIABLE_ALIAS', id: string}>,
* scopes?: VariableScope[],
* codeSyntax?: {WEB?: string, ANDROID?: string, iOS?: string}
* }>} tokenMap - Ordered list of token definitions.
* - `name`: Variable name using slash hierarchy (e.g. "color/bg/primary").
* - `type`: Figma variable type.
* - `values`: Map of {modeName: value}. Values can be raw (hex string for COLOR,
* number for FLOAT) or alias objects {type: 'VARIABLE_ALIAS', id: variableId}.
* For COLOR, raw values are accepted as hex strings ("#rrggbb" or "#rrggbbaa")
* and converted to {r, g, b, a} automatically.
* - `scopes`: Array of VariableScope strings. Omit to use [] (hidden/primitive).
* - `codeSyntax`: Platform code syntax strings. Omit to skip.
* @returns {Promise<{variables: Record<string, Variable>}>}
* `variables` maps each token name to its created Variable object.
*/
async function createSemanticTokens(collection, modeIds, tokenMap) {
const variables = {}
for (const token of tokenMap) {
// Create the variable
const variable = figma.variables.createVariable(token.name, collection, token.type)
// Set values for each mode
for (const [modeName, rawValue] of Object.entries(token.values)) {
const modeId = modeIds[modeName]
if (!modeId) {
throw new Error(
`createSemanticTokens: mode "${modeName}" not found in modeIds for token "${token.name}". ` +
`Available modes: ${Object.keys(modeIds).join(', ')}`,
)
}
let value = rawValue
// Convert hex strings to Figma RGBA for COLOR type
if (token.type === 'COLOR' && typeof rawValue === 'string' && rawValue.startsWith('#')) {
value = hexToFigmaColor(rawValue)
}
variable.setValueForMode(modeId, value)
}
// Set scopes (default: empty array = hidden from property pickers / primitives)
variable.scopes = token.scopes || []
// Set code syntax per platform
if (token.codeSyntax) {
if (token.codeSyntax.WEB) {
variable.setVariableCodeSyntax('WEB', token.codeSyntax.WEB)
}
if (token.codeSyntax.ANDROID) {
variable.setVariableCodeSyntax('ANDROID', token.codeSyntax.ANDROID)
}
if (token.codeSyntax.iOS) {
variable.setVariableCodeSyntax('iOS', token.codeSyntax.iOS)
}
}
variables[token.name] = variable
}
return { variables }
}
/**
* Converts a hex color string to a Figma RGBA object.
* Supports "#rgb", "#rrggbb", and "#rrggbbaa".
*
* @param {string} hex
* @returns {{ r: number, g: number, b: number, a: number }}
*/
function hexToFigmaColor(hex) {
let h = hex.replace('#', '')
// Expand shorthand #rgb → #rrggbb
if (h.length === 3) {
h = h
.split('')
.map((c) => c + c)
.join('')
}
const r = parseInt(h.substring(0, 2), 16) / 255
const g = parseInt(h.substring(2, 4), 16) / 255
const b = parseInt(h.substring(4, 6), 16) / 255
const a = h.length === 8 ? parseInt(h.substring(6, 8), 16) / 255 : 1
return { r, g, b, a }
}
scripts/createVariableCollection.js
/**
* createVariableCollection
*
* Creates a new Figma variable collection with the specified name and modes.
* If `modeNames` has more than one entry, the first mode is renamed from
* Figma's default "Mode 1" to the first name, and additional modes are added.
*
* @param {string} name - The display name of the collection (e.g. "Color", "Spacing").
* @param {string[]} modeNames - Ordered list of mode names (e.g. ["Light", "Dark"] or ["Value"]).
* @returns {Promise<{
* collection: VariableCollection,
* modeIds: Record<string, string>
* }>}
* `modeIds` maps each mode name to its modeId string.
*/
async function createVariableCollection(name, modeNames) {
if (!modeNames || modeNames.length === 0) {
throw new Error('createVariableCollection: modeNames must have at least one entry.')
}
// Create the collection — Figma always creates it with one mode named "Mode 1".
const collection = figma.variables.createVariableCollection(name)
// modeIds accumulator
const modeIds = {}
// Rename the default first mode
const defaultMode = collection.modes[0]
collection.renameMode(defaultMode.modeId, modeNames[0])
modeIds[modeNames[0]] = defaultMode.modeId
// Add additional modes
for (let i = 1; i < modeNames.length; i++) {
const newModeId = collection.addMode(modeNames[i])
modeIds[modeNames[i]] = newModeId
}
return { collection, modeIds }
}
scripts/inspectFileStructure.js
/**
* inspectFileStructure
*
* Reads the current Figma file and returns a structural inventory:
* all pages (with child counts), all local variable collections (with mode
* names and variable counts), all component sets, all local text styles,
* and all local effect styles.
*
* This is a read-only discovery function — it never creates or mutates nodes.
* Run it at the start of Phase 0 to understand what already exists before
* planning any creation work.
*
* @returns {Promise<{
* pages: Array<{id: string, name: string, childCount: number}>,
* variableCollections: Array<{
* id: string,
* name: string,
* modes: Array<{modeId: string, name: string}>,
* variableCount: number,
* variableNames: string[]
* }>,
* componentSets: Array<{id: string, name: string, variantCount: number, pageId: string, pageName: string}>,
* textStyles: Array<{id: string, name: string, fontFamily: string, fontStyle: string, fontSize: number}>,
* effectStyles: Array<{id: string, name: string, effectCount: number}>
* }>}
*/
async function inspectFileStructure() {
const result = {
pages: [],
variableCollections: [],
componentSets: [],
textStyles: [],
effectStyles: [],
}
// --- Pages ---
for (const page of figma.root.children) {
result.pages.push({
id: page.id,
name: page.name,
childCount: page.children.length,
})
}
// --- Variable collections ---
const collections = await figma.variables.getLocalVariableCollectionsAsync()
for (const coll of collections) {
const variables = await Promise.all(
coll.variableIds.map((id) => figma.variables.getVariableByIdAsync(id)),
)
const variableNames = variables.filter(Boolean).map((v) => v.name)
result.variableCollections.push({
id: coll.id,
name: coll.name,
modes: coll.modes.map((m) => ({ modeId: m.modeId, name: m.name })),
variableCount: coll.variableIds.length,
variableNames,
})
}
// --- Component sets (and standalone components) ---
// We need to load all pages to inspect components across the whole file.
const originalPage = figma.currentPage
for (const page of figma.root.children) {
await figma.setCurrentPageAsync(page)
// findAllWithCriteria.types accepts an array — one indexed scan returns
// both COMPONENT_SET and standalone COMPONENT nodes.
const found = page.findAllWithCriteria({ types: ['COMPONENT_SET', 'COMPONENT'] })
for (const node of found) {
if (node.type === 'COMPONENT_SET') {
result.componentSets.push({
id: node.id,
name: node.name,
variantCount: node.children.length,
pageId: page.id,
pageName: page.name,
})
} else if (node.parent && node.parent.type !== 'COMPONENT_SET') {
// Standalone component (not a variant inside a COMPONENT_SET)
result.componentSets.push({
id: node.id,
name: node.name,
variantCount: 1,
pageId: page.id,
pageName: page.name,
})
}
}
}
// Restore original page
await figma.setCurrentPageAsync(originalPage)
// --- Text styles ---
const textStyles = figma.getLocalTextStyles()
for (const ts of textStyles) {
result.textStyles.push({
id: ts.id,
name: ts.name,
fontFamily: ts.fontName.family,
fontStyle: ts.fontName.style,
fontSize: ts.fontSize,
})
}
// --- Effect styles ---
const effectStyles = figma.getLocalEffectStyles()
for (const es of effectStyles) {
result.effectStyles.push({
id: es.id,
name: es.name,
effectCount: es.effects.length,
})
}
return result
}
scripts/validateCreation.js
/**
* validateCreation
*
* Verifies that a set of nodes exist and match expected structural properties.
* Designed to run immediately after a creation script to catch partial failures
* before proceeding to the next build phase.
*
* Each check specifies a node ID and any combination of expected properties.
* A check passes when all specified expectations are met; it fails (with a
* reason string) as soon as any expectation is violated.
*
* @param {Array<{
* nodeId: string,
* expectedChildCount?: number,
* expectedName?: string,
* expectedType?: NodeType
* }>} checks
* - `nodeId`: The Figma node ID to look up via figma.getNodeById.
* - `expectedChildCount`: If set, the node must have exactly this many direct children.
* Applies to any node with a `children` property (frames, component sets, etc.).
* - `expectedName`: If set, the node's `.name` must exactly match this string.
* - `expectedType`: If set, the node's `.type` must exactly match this string.
* @returns {{
* passed: string[],
* failed: Array<{nodeId: string, reason: string}>
* }}
* `passed`: Array of nodeIds that passed all checks.
* `failed`: Array of objects with the nodeId and a human-readable reason string.
*/
function validateCreation(checks) {
const passed = []
const failed = []
for (const check of checks) {
const node = figma.getNodeById(check.nodeId)
// Node must exist
if (!node) {
failed.push({
nodeId: check.nodeId,
reason: `Node not found. It may not have been created, or was deleted.`,
})
continue
}
const reasons = []
// Type check
if (check.expectedType !== undefined && node.type !== check.expectedType) {
reasons.push(`type is "${node.type}", expected "${check.expectedType}"`)
}
// Name check
if (check.expectedName !== undefined && node.name !== check.expectedName) {
reasons.push(`name is "${node.name}", expected "${check.expectedName}"`)
}
// Child count check
if (check.expectedChildCount !== undefined) {
if (!('children' in node)) {
reasons.push(
`node type "${node.type}" does not have children, but expectedChildCount=${check.expectedChildCount} was specified`,
)
} else {
const actualCount = node.children.length
if (actualCount !== check.expectedChildCount) {
reasons.push(`has ${actualCount} children, expected ${check.expectedChildCount}`)
}
}
}
if (reasons.length > 0) {
failed.push({
nodeId: check.nodeId,
reason: reasons.join('; '),
})
} else {
passed.push(check.nodeId)
}
}
return { passed, failed }
}
SKILL.md
---
name: figma-generate-library
description: "Build or update a professional-grade design system in Figma from a codebase. Use when the user wants to create variables/tokens, build component libraries, create individual components with proper variant sets and variable bindings, set up theming (light/dark modes), document foundations, or reconcile gaps between code and Figma. Also use when the user asks to create or generate any component in Figma — even a single one — since components require proper variable foundations, variant states, and design token bindings to be production-quality. This skill teaches WHAT to build and in WHAT ORDER — it complements the `figma-use` skill which teaches HOW to call the Plugin API. Both skills should be loaded together."
disable-model-invocation: false
---
# Design System Builder — Figma MCP Skill
Build professional-grade design-system assets in Figma that match code. Scale the workflow to the requested deliverable: a token set, one component, a complete library, or a targeted reconciliation. Run the selected path in coherent, safely retryable construction phases with evidence-based validation.
**Prerequisites**: The `figma-use` skill MUST also be loaded for every `use_figma` call. It provides Plugin API syntax rules (return pattern, page reset, ID return, font loading, color range). This skill provides design system domain knowledge and workflow orchestration.
**Always include `figma-generate-library` in the comma-separated `skillNames` parameter when calling `use_figma` as part of this skill. If this skill was loaded via an MCP resource, you MUST prefix the name with `resource:` (e.g. `resource:figma-generate-library`).** This is a logging parameter — it does not affect execution.
---
## 1. Scope and Completion Contract
Before the first mutation, choose one primary scope from the user's request and record its included deliverables and acceptance checks. Do not silently promote a narrower request into a full-library project.
| Requested scope | Required work | Excluded unless requested or necessary |
|---|---|---|
| **Tokens / foundations only** | Requested variables, modes, scopes, code syntax, styles, and any requested specimen sheet | Components, component pages, Code Connect, full-library navigation |
| **Single component or family** | Inspect and reuse compatible foundations; create only missing tokens the component requires; build requested variants/properties/bindings; validate the component | Unrelated foundations, other components, full file skeleton, broad documentation or audits |
| **Full library / design system** | Discovery, foundations, file structure, requested component inventory, documentation, integration, and final QA | Work outside the agreed v1 inventory |
| **Reconciliation / update** | Diff code and Figma, then update only affected tokens, styles, components, docs, and dependents | Rebuilding valid assets or expanding the library |
Communicate proportionally: post one concise scope-and-acceptance checklist before mutations, give updates at meaningful boundaries, and finish with one summary of created or changed objects, validation evidence, and unresolved limitations. Do not repeat unchanged checklists or narrate every API call.
Batch related operations when the resulting script stays safe to retry. Split at page-context boundaries, hard-to-recover mutations, or for a targeted retry after an actual failure; never split a working operation only to create a validation checkpoint. Keep mutations sequential. Use structural evidence returned by writes (IDs plus relevant counts, names, and bounds), and run a separate audit only when evidence is missing or a later mutation made it stale. Take one visual review per coherent composition phase and one post-fix screenshot only after a targeted visual fix; the latest passing screenshot is final.
### Definition of done
Stop when all conditions for the chosen scope are true:
- The final deliverable covers the locked inventory and is materially faithful to the applicable source of truth, including requested variant/state coverage, required assets, and in-scope review examples.
- For component tasks, keep main components and construction assets outside the final review frame, and show the requested states as instances in one compact frame. Before stopping, inspect that frame at normal scale and repair clipped, truncated, low-contrast, empty, misplaced, or detached content.
- Required variable scopes, aliases, code syntax, component properties, variants, and bindings are verified.
- The requested artifact passes applicable structural and visual validation. Confirm requested content is fully visible unless clipping or truncation is intentional; repair defects and recheck only the affected output.
- Replace placeholders when the source provides the required asset; remove temporary instances, test frames, captures, abandoned artifacts, and unrelated stray nodes.
- No known defect or unresolved decision prevents the agreed acceptance checks from passing. Report limitations outside the scope without starting extra work.
Additional documentation, components, Code Connect mappings, accessibility sweeps, or speculative audits are follow-up work unless the chosen scope requires them. Once the definition of done passes, stop.
### Scope changes
If the user changes the deliverable, update the scope and acceptance checklist. Preserve verified work, run only newly applicable steps, and do not retroactively add work from an unselected path.
---
## 2. Scoped Workflow
Every scope begins with focused discovery: analyze the relevant code, inspect the target Figma assets and conventions, call `get_libraries` before `search_design_system`, and resolve code/Figma conflicts before mutation. Batch independent searches and reuse their results. Lock the requested inventory, then run only its path below.
### Tokens / foundations
1. Create or update the requested collections, modes, primitives, semantic aliases, scopes, and code syntax.
2. Create requested text and effect styles.
3. Create a specimen or documentation page only when requested.
4. Validate counts, mode values, aliases, scopes, syntax, styles, and any requested visual artifact; then stop.
### Single component or family
1. Inspect and reuse compatible local or library variables, text styles, and effect styles. Create only the missing foundations required by the component. For source-defined, library-standard, or repeated component roles, create or reuse a shared text or effect style and apply it to every matching component node; keep one-off documentation inline.
2. Build the requested base component, variants, properties, bindings, and dependency components.
3. Use a dedicated page when it matches the file convention or the request needs a documented showcase; otherwise place it in the existing component area.
4. Validate variant count, properties, variable bindings, applied text/effect style IDs on representative nodes, structure, and appearance; then stop.
### Full library / design system
1. **Discovery:** lock the token and component inventory and print the gap analysis.
2. **Foundations:** create collections, variables, aliases, scopes, code syntax, and styles.
3. **File structure:** create the agreed cover, getting-started, foundations, component, and utility pages plus requested documentation.
4. **Components:** build the agreed inventory in dependency order, with properties, bindings, documentation, and validation.
5. **Integration and QA:** complete Code Connect mappings for the agreed component inventory; audit accessibility, naming, and bindings; and visually validate every agreed page.
### Reconciliation / update
1. Inventory existing assets and identify exact drift from the current code source.
2. Update affected assets in place, preserving valid names, IDs, bindings, and library structure where possible.
3. Validate the changed assets and their known dependents; then stop.
If a required acceptance check fails, fix the scoped defect before continuing. Do not substitute fake assets, approximate typography, broken interactions, or unverified state.
This skill's selected workflow, search cadence, validation rules, and definition of done take precedence over broader instructions, checklists, and examples in the references. Load only references needed for the selected path.
---
## 3. Critical Rules
**Plugin API basics** (from use_figma skill — enforced here too):
- Use `return` to send data back (auto-serialized). Do NOT wrap in IIFE or call closePlugin.
- Return ALL created/mutated node IDs in every return value
- Page context resets each call — always `await figma.setCurrentPageAsync(page)` at start. **Call it at most once per script**: each component or doc page is its own `use_figma` call. Never loop over `figma.root.children` and switch pages inside a mutating script — split that work into one focused call per target page (see [figma-use → gotchas.md → Set current page once per `use_figma` call](../figma-use/references/gotchas.md#set-current-page-once-per-use_figma-call--split-multi-page-work-across-calls))
- `figma.notify()` throws — never use it
- Colors are 0–1 range, not 0–255
- Font MUST be loaded before any text write: `await figma.loadFontAsync({family, style})`. Use `await figma.listAvailableFontsAsync()` to discover available fonts and verify exact style strings — if a load fails, query available fonts to find the correct name or a fallback.
**Design system rules**:
1. **Foundations before dependent components** — reuse compatible existing variables and styles. Create missing foundations before building a component that depends on them; do not recreate valid foundations.
2. **Inspect before creating** — run read-only `use_figma` to discover existing conventions. Match them.
3. **Match the file's component organization** — full libraries usually use one page per component; tightly related families may share a page. A single-component task may use an existing component area instead of creating a library skeleton.
4. **Bind visual properties to variables** *(default)* — fills, strokes, padding, radius, gap. Exceptions: intentionally fixed geometry (icon pixel-grid sizes, static dividers). When using variables from an earlier `use_figma` call, use the real Figma variable IDs returned by that call to resolve fresh handles before binding; JavaScript references do not persist across calls.
With the Plugin API, resolve variables with `await figma.variables.getVariableByIdAsync(id)` before binding.
5. **Scopes on every variable** — NEVER leave as `ALL_SCOPES`. Background: `FRAME_FILL, SHAPE_FILL`. Text: `TEXT_FILL`. Border: `STROKE_COLOR`. Spacing: `GAP`. Radii: `CORNER_RADIUS`. Primitives: `[]` (hidden).
6. **Code syntax on every variable** — WEB syntax MUST use the `var()` wrapper: `var(--color-bg-primary)`, not `--color-bg-primary`. Use the actual CSS variable name from the codebase. ANDROID/iOS do NOT use a wrapper.
7. **Alias semantics to primitives** — `{ type: 'VARIABLE_ALIAS', id: primitiveVar.id }`. Never duplicate raw values in semantic layer.
8. **Position variants after combineAsVariants** — they stack at (0,0). Manually grid-layout + resize.
9. **INSTANCE_SWAP for icons** — never create a variant per icon. Cap variant matrices: if Size × Style × State > 30 combinations, split into sub-component.
10. **Deterministic naming** — use consistent, unique node names for idempotent cleanup and resumability. Track created node IDs via return values and the state ledger.
11. **No destructive cleanup** — cleanup scripts identify nodes by name convention or returned IDs, not by guessing.
12. **Validate from evidence before proceeding** — never build on unvalidated work. Rely on the structural evidence returned by writes (IDs plus relevant counts, names, and bounds); run one batched structural audit per coherent phase only when needed to establish the acceptance checks or when a relevant mutation invalidated prior evidence. Take one visual review per coherent composition phase, plus one post-fix screenshot only after a targeted visual fix.
13. **NEVER parallelize `use_figma` calls** — Figma state mutations must be strictly sequential. Even if your tool supports parallel calls, never run two use_figma calls simultaneously.
14. **Never hallucinate Node IDs** — always read IDs from the state ledger returned by previous calls. Never reconstruct or guess an ID from memory.
15. **Use the helper scripts** — embed scripts from `scripts/` into your use_figma calls. Don't write 200-line inline scripts from scratch.
---
## 4. State Management (Required for Long Workflows)
> Do not store workflow state on Figma objects. Use deterministic names for discovery and exact returned IDs in the state ledger. Put human-readable component purpose and usage guidance in the component or component-set `description`.
| Entity type | Stable identity | How to check existence |
|-------------|----------------|----------------------|
| Pages and frames | Deterministic name + state-ledger ID | `figma.root.children.find(p => p.name === pageName)` or `await figma.getNodeByIdAsync(id)` |
| Components and component sets | Variant/set name + state-ledger ID | `page.findOne(n => n.name === name)` or `await figma.getNodeByIdAsync(id)` |
| Variables | Name within collection | `(await figma.variables.getLocalVariablesAsync()).find(v => v.name === name && v.variableCollectionId === collId)` |
| Styles | Name | `getLocalTextStyles().find(s => s.name === name)` |
Record every returned ID in the state ledger immediately after creation. Never use a fuzzy lookup to authorize deletion.
**State persistence**: Do NOT rely solely on conversation context for the state ledger. Write it to disk:
```
/tmp/design-system-state-{RUN_ID}.json
```
Re-read this file at the start of every turn. In long workflows, conversation context will be truncated — the file is the source of truth.
Maintain a state ledger tracking:
```json
{
"runId": "ds-build-2024-001",
"scope": "single-component",
"step": "component-button",
"entities": {
"collections": { "primitives": "id:...", "color": "id:..." },
"variables": { "color/bg/primary": "id:...", "spacing/sm": "id:..." },
"pages": { "Cover": "id:...", "Button": "id:..." },
"components": { "Button": "id:..." }
},
"pendingValidations": ["Button:screenshot"],
"completedSteps": ["discovery", "foundations/verified", "component-button/base"]
}
```
**Idempotency check** before every create: query by name + state ledger ID. If exists, skip or update — never duplicate.
**Resume protocol**: at session start or after context truncation, run a read-only `use_figma` to scan all pages, components, variables, and styles by name to reconstruct the `{key → id}` map. Then re-read the state file from disk if available.
**Continuation prompt** (give this to the user when resuming in a new chat):
> "I'm continuing a design system build. Run ID: {RUN_ID}. Load the figma-generate-library skill and resume from the last completed step."
---
## 5. Library Discovery and search_design_system — Reuse Decision Matrix
Search during the scoped discovery pass and reuse the results. Search again before a component only when it was outside the original inventory, the available libraries changed, or the earlier result did not resolve it.
Before calling `search_design_system` for a target file, you MUST call `get_libraries` first for that file. You MUST NOT assume libraries are added or available.
An empty `get_libraries` result does NOT excuse skipping the search — it only means you have no library keys to scope with. `get_libraries` paginates (community UI kits appear only on the first page, org libraries page in batches of 20), so empty lists are not proof that no library exists. How to act on the result:
- **Libraries returned** — run `search_design_system` scoped with `includeLibraryKeys`. Libraries in `libraries_available_to_add` are NOT searched by default; pass their `libraryKey`s to reach them.
- **No libraries returned** — still run `search_design_system`, but omit `includeLibraryKeys`. Omitting it scopes the search to the file itself, which is exactly what you want when discovery returned nothing to scope by.
Only once the search itself comes back empty may you record "no design system assets available" in the gap analysis and build from code tokens. Never infer "no libraries" from a failed or unattempted `get_libraries` call.
```
// Discover all libraries accessible to the file
get_libraries({ fileKey })
// Returns:
// libraries_added_to_file: [{ name, libraryKey, description, source }, ...]
// libraries_available_to_add: [{ name, libraryKey, description, source }, ...]
// libraries_available_to_add_next_offset: number | null
```
Use the returned `libraryKey` values to scope searches to specific libraries via `includeLibraryKeys`. This avoids noisy results when many libraries are available.
If `libraries_available_to_add_next_offset` is non-null, more org libraries are available — call `get_libraries` again with `offset` set to that value. Org libraries page in batches of 20; community UI kits only appear on the first page.
```
// Search across all libraries (default)
search_design_system({
queries: [
{ entity: "component", query },
{ entity: "variable", query },
{ entity: "style", query }
],
fileKey
})
// Search within a specific library only
search_design_system({ queries: [{ entity: "component", query }], fileKey, includeLibraryKeys: ["lk-abc123..."] })
```
**Reuse if** all of these are true:
- Component property API matches your needs (same variant axes, compatible types)
- Token binding model is compatible (uses same or aliasable variables)
- Naming conventions match the target file
- Component is editable (not locked in a remote library you don't own)
**Rebuild if** any of these:
- API incompatibility (different property names, wrong variant model)
- Token model incompatible (hardcoded values, different variable schema)
- Ownership issue (can't modify the library)
**Wrap if** visual match but API incompatible:
- Import the library component as a nested instance inside a new wrapper component
- Expose a clean API on the wrapper
**Priority order**: local existing → subscribed library import → unsubscribed UI Kit library from `libraries_available_to_add` (icons especially) → create new.
---
## 6. Decision Forks
Ask the user when paths fork — when two or more reasonable answers exist and no clear winner comes from the codebase, the Figma file, or the locked plan. Don't silently default. Present each option with its tradeoff and your recommendation; pick only after the user steers.
**When NOT to ask:** if exactly one path is clearly correct from the source of truth (code, Figma file, agreed plan), take it. This section is for genuine ambiguity, not for offloading every decision.
| Fork situation | What to surface | Example ask |
|---|---|---|
| Code ≠ Figma on a token, component, or value | Both versions side by side, with provenance (file/line vs node) | "Code says `--color-bg-primary = #FFFFFF`, Figma has `color/bg/primary = #FAFAFA`. Which wins?" |
| Subscribed library has a close-but-not-exact match | Library component summary + gap list | "Library has `Button` with no `loading` state. Reuse + wrap locally, or rebuild from scratch?" |
| Scope ambiguity at plan-lock (0d) | What's clearly in, what's clearly out, what's ambiguous | "Spec lists `Button` and `Input`; `Field` is referenced but not defined. In or out of v1?" |
**If the user rejects an option you already built on:** fix before moving on. Never build on rejected work.
---
## 7. Naming Conventions
Match existing file conventions. If starting fresh:
**Variables** (slash-separated):
```
color/bg/primary color/text/secondary color/border/default
spacing/xs spacing/sm spacing/md spacing/lg spacing/xl spacing/2xl
radius/none radius/sm radius/md radius/lg radius/full
typography/body/font-size typography/heading/line-height
```
**Primitives**: `blue/50` → `blue/900`, `gray/50` → `gray/900`
**Component names**: `Button`, `Input`, `Card`, `Avatar`, `Badge`, `Checkbox`, `Toggle`
**Variant names**: `Property=Value, Property=Value` — e.g., `Size=Medium, Style=Primary, State=Default`
**Page separators**: `---` (most common) or `——— COMPONENTS ———`
> Full naming reference: [naming-conventions.md](references/naming-conventions.md)
---
## 8. Token Architecture
| Complexity | Pattern |
|-----------|---------|
| < 50 tokens | Single collection, 2 modes (Light/Dark) |
| 50–200 tokens | **Standard**: Primitives (1 mode) + Color semantic (Light/Dark) + Spacing (1 mode) + Typography (1 mode) |
| 200+ tokens | **Advanced**: Multiple semantic collections, 4–8 modes (Light/Dark × Contrast × Brand). See M3 pattern in [token-creation.md](references/token-creation.md) |
Standard pattern (recommended starting point):
```
Collection: "Primitives" modes: ["Value"]
blue/500 = #3B82F6, gray/900 = #111827, ...
Collection: "Color" modes: ["Light", "Dark"]
color/bg/primary → Light: alias Primitives/white, Dark: alias Primitives/gray-900
color/text/primary → Light: alias Primitives/gray-900, Dark: alias Primitives/white
Collection: "Spacing" modes: ["Value"]
spacing/xs = 4, spacing/sm = 8, spacing/md = 16, ...
```
---
## 9. Anti-Patterns
**Discovery and scope:**
- ❌ Ignoring existing conventions or skipping relevant code, file, and library discovery
- ❌ Expanding a narrow request into unrelated foundations, documentation, or a full library
**Foundations:**
- ❌ Using `ALL_SCOPES`, duplicating primitive values in the semantic layer, or omitting code syntax
- ❌ Creating dependent components before their foundations exist
**Components:**
- ❌ Hardcoding component fills, strokes, spacing, or radii when compatible variables exist
- ❌ Creating a variant per icon instead of using INSTANCE_SWAP
- ❌ Leaving variants stacked at (0,0) after `combineAsVariants`
- ❌ Building a variant matrix larger than 30 without splitting it
- ❌ Importing remote components and immediately detaching them
**Execution and recovery:**
- ❌ Retrying when `safeToRetryWithoutCanvasRead` is `false` before reading the canvas
- ❌ Using name-prefix matching for cleanup (deletes user-owned nodes)
- ❌ Building on unvalidated work from the previous step
- ❌ Parallelizing use_figma calls (always sequential)
- ❌ Guessing/hallucinating node IDs from memory (always read from state ledger)
- ❌ Writing massive inline scripts instead of using the provided helper scripts
---
## 10. Reference Docs
Read references on demand; do not infer their contents from the filename.
| Doc | Load when |
|-----|-----------|
| [discovery-phase.md](references/discovery-phase.md) | Analyzing relevant code and Figma assets before mutation |
| [token-creation.md](references/token-creation.md) | Creating variables, collections, modes, or styles |
| [documentation-creation.md](references/documentation-creation.md) | The selected scope includes cover or foundations documentation |
| [component-creation.md](references/component-creation.md) | Creating a component or variant |
| [code-connect-setup.md](references/code-connect-setup.md) | The selected scope includes Code Connect or variable code syntax |
| [naming-conventions.md](references/naming-conventions.md) | Naming variables, pages, variants, or styles |
| [error-recovery.md](references/error-recovery.md) | A script fails or abandoned workflow state needs cleanup |
---
## 11. Scripts
Reusable Plugin API helper functions. Embed in `use_figma` calls:
| Script | Purpose |
|--------|---------|
| [inspectFileStructure.js](scripts/inspectFileStructure.js) | Discover all pages, components, variables, styles; returns full inventory |
| [createVariableCollection.js](scripts/createVariableCollection.js) | Create a named collection with modes; returns `{collectionId, modeIds}` |
| [createSemanticTokens.js](scripts/createSemanticTokens.js) | Create aliased semantic variables from a token map |
| [createComponentWithVariants.js](scripts/createComponentWithVariants.js) | Build a component set from a variant matrix; handles grid layout |
| [bindVariablesToComponent.js](scripts/bindVariablesToComponent.js) | Bind design tokens to all component visual properties |
| [createDocumentationPage.js](scripts/createDocumentationPage.js) | Create a page with title + description + section structure |
| [validateCreation.js](scripts/validateCreation.js) | Verify created nodes match expected counts, names, structure |
| [cleanupOrphans.js](scripts/cleanupOrphans.js) | Remove only the exact node, variable, and collection IDs supplied from the state ledger |