
Figma Generate Library
- 2.7k installs
- 1.8k repo stars
- Updated July 30, 2026
- figma/mcp-server-guide
figma-generate-library is a skill for phased Figma design system builds from codebase tokens through components and QA.
About
figma-generate-library builds professional-grade Figma design systems that match codebases across discovery, foundations, file structure, components, and integration QA. Phase zero analyzes code tokens, inspects the Figma file, searches subscribed libraries, and locks v1 scope before any writes. Phase one creates variable collections, primitive and semantic tokens, scopes, code syntax, effect styles, and typography with mandatory user checkpoints. Phase two creates page skeletons and foundations documentation. Phase three builds one component at a time with auto-layout, variant grids, properties, documentation, and screenshot validation. Phase four finalizes Code Connect, accessibility audits, naming audits, and unresolved binding checks. Critical rules require variables before components, one page per component by default, bind fills and spacing to variables, and never parallelize use_figma mutations. The skill complements figma-use for Plugin API syntax and expects twenty to one hundred plus sequential calls.
- Mandatory phased workflow with user checkpoints between phases.
- Variables and tokens before any component creation.
- One component per page with variant grid validation.
- Scopes and code syntax required on every variable.
- Never parallelize use_figma state mutations.
Figma Generate Library by the numbers
- 2,748 all-time installs (skills.sh)
- +94 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #119 of 1,888 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
figma-generate-library capabilities & compatibility
- Capabilities
- codebase token discovery and scope locking · primitive and semantic variable creation with sc · phased component build with variant grids · code connect mapping during component context · accessibility contrast and touch target audits · sequential use_figma orchestration with validati
- Works with
- figma
- Use cases
- ui design · frontend
- Pricing
- Free
What figma-generate-library says it does
This is NEVER a one-shot task.
Variables BEFORE components
npx skills add https://github.com/figma/mcp-server-guide --skill figma-generate-libraryAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.7k |
|---|---|
| repo stars | ★ 1.8k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 30, 2026 |
| Repository | figma/mcp-server-guide ↗ |
How do I build a production-grade Figma design system that matches my codebase tokens and components?
Orchestrate multi-phase Figma design system builds from codebase tokens through variables, components, and QA checkpoints.
Who is it for?
Design system leads reconciling code tokens with Figma variables and component libraries.
Skip if: Skip for single-component tweaks without token foundations or non-Figma design tools.
When should I use this skill?
User builds Figma variables, component libraries, theming, or code-to-Figma design system gaps.
What you get
Scoped token foundations, component library pages, and validated bindings aligned to code.
- Figma variable collections
- Component library with variants
- Foundations documentation pages
By the numbers
- Orchestrates 20–100+ use_figma Plugin API calls across phased workflows
- Defines 5 sequential phases from Discovery through Integration QA
- References 4 production design systems: Material 3, Polaris, Figma UI3, and Simple DS
Files
Design System Builder — Figma MCP Skill
Build professional-grade design systems in Figma that match code. This skill orchestrates multi-phase workflows across 20–100+ use_figma calls, enforcing quality patterns from real-world design systems (Material 3, Polaris, Figma UI3, Simple DS).
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. The One Rule That Matters Most
This is NEVER a one-shot task. Building a design system requires 20–100+ use_figma calls across multiple phases, with mandatory user checkpoints between them. Any attempt to create everything in one call WILL produce broken, incomplete, or unrecoverable results. Break every operation to the smallest useful unit, validate, get feedback, proceed.
---
2. Mandatory Workflow
Every design system build follows this phase order. Skipping or reordering phases causes structural failures that are expensive to undo.
Phase 0: DISCOVERY (always first — no use_figma writes yet)
0a. Analyze codebase → extract tokens, components, naming conventions
0b. Inspect Figma file → pages, variables, components, styles, existing conventions
0c. Search subscribed libraries → use search_design_system for reusable assets
0d. Lock v1 scope → agree on exact token set + component list before any creation
0e. Map code → Figma → resolve conflicts (code and Figma disagree = ask user)
✋ USER CHECKPOINT: present full plan, await explicit approval
Phase 1: FOUNDATIONS (tokens first — always before components)
1a. Create variable collections and modes
1b. Create primitive variables (raw values, 1 mode)
1c. Create semantic variables (aliased to primitives, mode-aware)
1d. Set scopes on ALL variables
1e. Set code syntax on ALL variables
1f. Create effect styles (shadows) and text styles (typography)
→ Exit criteria: every token from the agreed plan exists, all scopes set, all code syntax set
✋ USER CHECKPOINT: show variable summary, await approval
Phase 2: FILE STRUCTURE (before components)
2a. Create page skeleton: Cover → Getting Started → Foundations → --- → Components → --- → Utilities
2b. Create foundations documentation pages (color swatches, type specimens, spacing bars)
→ Exit criteria: all planned pages exist, foundations docs are navigable
✋ USER CHECKPOINT: show page list + screenshot, await approval
Phase 3: COMPONENTS (one at a time — never batch)
For EACH component (in dependency order: atoms before molecules):
3a. Create dedicated page
3b. Build base component with auto-layout + full variable bindings
3c. Create all variant combinations (combineAsVariants + grid layout)
3d. Add component properties (TEXT, BOOLEAN, INSTANCE_SWAP)
3e. Link properties to child nodes
3f. Add page documentation (title, description, usage notes)
3g. Validate: get_metadata (structure) + get_screenshot (visual)
3h. Optional: lightweight Code Connect mapping while context is fresh
→ Exit criteria: variant count correct, all bindings verified, screenshot looks right
✋ USER CHECKPOINT per component: show screenshot, await approval before next component
Phase 4: INTEGRATION + QA (final pass)
4a. Finalize all Code Connect mappings
4b. Accessibility audit (contrast, min touch targets, focus visibility)
4c. Naming audit (no duplicates, no unnamed nodes, consistent casing)
4d. Unresolved bindings audit (no hardcoded fills/strokes remaining)
4e. Final review screenshots of every page
✋ USER CHECKPOINT: complete sign-off---
3. Critical Rules
Plugin API basics (from use_figma skill — enforced here too):
- Use
returnto 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 ownuse_figmacall. Never loop overfigma.root.childrenand 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.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}). Useawait 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. Variables BEFORE components — components bind to variables. No token = no component. 2. Inspect before creating — run read-only use_figma to discover existing conventions. Match them. 3. One page per component (default) — exception: tightly related families (e.g., Input + helpers) may share a page with clear section separation. 4. Bind visual properties to variables (default) — fills, strokes, padding, radius, gap. Exceptions: intentionally fixed geometry (icon pixel-grid sizes, static dividers). 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 before proceeding — never build on unvalidated work. get_metadata after every create, get_screenshot after each component. 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. 16. Explicit phase approval — at each checkpoint, name the next phase explicitly. "looks good" is not approval to proceed to Phase 3 if you asked about Phase 1.
---
4. State Management (Required for Long Workflows)
`getPluginData()` / `setPluginData()` are NOT supported in `use_figma`. UsegetSharedPluginData()/setSharedPluginData()instead (these ARE supported), or use name-based lookups and the state ledger (returned IDs).
| Entity type | Idempotency key | How to check existence |
|---|---|---|
| Scene nodes (pages, frames, components) | setSharedPluginData('dsb', 'key', value) or unique name | node.getSharedPluginData('dsb', 'key') or page.findOne(n => n.name === 'Button') |
| Variables | Name within collection | (await figma.variables.getLocalVariablesAsync()).find(v => v.name === name && v.variableCollectionId === collId) |
| Styles | Name | getLocalTextStyles().find(s => s.name === name) |
Tag every created scene node immediately after creation:
node.setSharedPluginData('dsb', 'run_id', RUN_ID); // identifies this build run
node.setSharedPluginData('dsb', 'phase', 'phase3'); // which phase created it
node.setSharedPluginData('dsb', 'key', 'component/button');// unique logical keyState persistence: Do NOT rely solely on conversation context for the state ledger. Write it to disk:
/tmp/dsb-state-{RUN_ID}.jsonRe-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:
{
"runId": "ds-build-2024-001",
"phase": "phase3",
"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": ["phase0", "phase1", "phase2", "component-avatar"]
}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 FIRST in Phase 0, then again immediately before each component creation.
Start with `get_libraries` to understand what libraries are available before searching blindly:
// 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 | nullUse 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({ query, fileKey, includeComponents: true, includeVariables: true, includeStyles: true })
// Search within a specific library only
search_design_system({ query, fileKey, includeLibraryKeys: ["lk-abc123..."], includeComponents: true })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
Three-way priority: local existing → subscribed library import → create new.
---
6. User Checkpoints
Mandatory. Design decisions require human judgment.
| After | Required artifacts | Ask |
|---|---|---|
| Discovery + scope lock | Token list, component list, gap analysis | "Here's my plan. Approve before I create anything?" |
| Foundations | Variable summary (N collections, M vars, K modes), style list | "All tokens created. Review before file structure?" |
| File structure | Page list + screenshot | "Pages set up. Review before components?" |
| Each component | get_screenshot of component page | "Here's [Component] with N variants. Correct?" |
| Each conflict (code ≠ Figma) | Show both versions | "Code says X, Figma has Y. Which wins?" |
| Final QA | Per-page screenshots + audit report | "Complete. Sign off?" |
If user rejects: 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-heightPrimitives: 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
---
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 |
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. Per-Phase Anti-Patterns
Phase 0 anti-patterns:
- ❌ Starting to create anything before scope is locked with user
- ❌ Ignoring existing file conventions and imposing new ones
- ❌ Skipping
search_design_systembefore planning component creation
Phase 1 anti-patterns:
- ❌ Using
ALL_SCOPESon any variable - ❌ Duplicating raw values in semantic layer instead of aliasing
- ❌ Not setting code syntax (breaks Dev Mode and round-tripping)
- ❌ Creating component tokens before agreeing on token taxonomy
Phase 2 anti-patterns:
- ❌ Skipping the cover page or foundations docs
- ❌ Putting multiple unrelated components on one page
Phase 3 anti-patterns:
- ❌ Creating components before foundations exist
- ❌ Hardcoding any fill/stroke/spacing/radius value in a component
- ❌ Creating a variant per icon (use INSTANCE_SWAP instead)
- ❌ Not positioning variants after combineAsVariants (they all stack at 0,0)
- ❌ Building variant matrix > 30 without splitting (variant explosion)
- ❌ Importing remote components then immediately detaching them
General anti-patterns:
- ❌ Retrying a failed script without understanding the error first
- ❌ Using name-prefix matching for cleanup (deletes user-owned nodes)
- ❌ Building on unvalidated work from the previous step
- ❌ Skipping user checkpoints to "save time"
- ❌ 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
- ❌ Starting Phase 3 because the user said "build the button" without completing Phases 0-2
---
10. Reference Docs
Load on demand — each reference is authoritative for its phase:
Use your file reading tool to read these docs when needed. Do not assume their contents from the filename.
| Doc | Phase | Required / Optional | Load when |
|---|---|---|---|
| discovery-phase.md | 0 | Required | Starting any build — codebase analysis + Figma inspection |
| token-creation.md | 1 | Required | Creating variables, collections, modes, styles |
| documentation-creation.md | 2 | Required | Creating cover page, foundations docs, swatches |
| component-creation.md | 3 | Required | Creating any component or variant |
| code-connect-setup.md | 3–4 | Required | Setting up Code Connect or variable code syntax |
| naming-conventions.md | Any | Optional | Naming anything — variables, pages, variants, styles |
| error-recovery.md | Any | Required on error | Script fails, multi-step workflow recovery, cleanup of abandoned workflow state |
---
11. Scripts
Reusable Plugin API helper functions. Embed in use_figma calls:
| Script | Purpose |
|---|---|
| inspectFileStructure.js | Discover all pages, components, variables, styles; returns full inventory |
| createVariableCollection.js | Create a named collection with modes; returns {collectionId, modeIds} |
| createSemanticTokens.js | Create aliased semantic variables from a token map |
| createComponentWithVariants.js | Build a component set from a variant matrix; handles grid layout |
| bindVariablesToComponent.js | Bind design tokens to all component visual properties |
| createDocumentationPage.js | Create a page with title + description + section structure |
| validateCreation.js | Verify created nodes match expected counts, names, structure |
| cleanupOrphans.js | Remove orphaned nodes by name convention or state ledger IDs |
| rehydrateState.js | Scan file for all pages, components, variables by name; returns full {key → nodeId} map for state reconstruction |
Part of the figma-generate-library skill.
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 |
CODE_CONNECT_ASSET_NOT_FOUND | Published component not found | Ensure the component is published to the library |
CODE_CONNECT_INSUFFICIENT_PERMISSIONS | No edit access | Request edit permission on the file |
CODE_CONNECT_NO_LIBRARY_FOUND | File is not published as a library | Publish the file as a Figma library first |
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_contextis 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 and call add_code_connect_map individually for any failures.
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:
// 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:
// 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_suggestionsto 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_maprequires the component to be published to a library. If the file is not yet published, the mapping will fail withCODE_CONNECT_NO_LIBRARY_FOUND. - 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
templateparameter 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). Addtemplateonly if the user needs precise prop-level snippet rendering.
Part of the figma-generate-library skill.
Component Creation Reference
Complete guide for Phase 3: building components with variant matrices, variable bindings, component properties, and documentation.
Design files only. Every snippet here (includingfigma.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 useInterbecause it's available everywhere;loadFontAsyncis 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, FormIf a component embeds an instance of another component, the embedded component must be created first. Build your dependency graph during Phase 0 and encode the creation order in the plan.
Building Blocks Sub-Components (M3 Pattern)
For complex components with independent sub-element state machines, extract the sub-element into its own component set prefixed with Building Blocks/ (public) or .Building Blocks/ (hidden from assets panel). The dot-prefix is a Figma convention for suppressing a component from the public assets panel.
When to use Building Blocks:
- The sub-element has its own variant axes (state, selection) that would cause combinatorial explosion in the parent
- The sub-element repeats (nav items, table cells, calendar cells, segmented button segments)
- The sub-element has different variant axes than the parent
Example (M3 Segmented Button):
Building Blocks/Segmented button/Button segment (start) [27 variants: Config × State × Selected]
Building Blocks/Segmented button/Button segment (middle) [27 variants]
Building Blocks/Segmented button/Button segment (end) [27 variants]
Segmented button [16 variants: Segments=2-5 × Density=0/-1/-2/-3]
Each variant contains instances of the appropriate Building Block segment components.The parent manages composition and configuration; the Building Block manages its own interaction states.
Private Components (__ Prefix)
Use the __ prefix for internal helper components that should not appear in the team library (Shop Minis pattern). Use _ for documentation-only components (UI3 pattern).
__asset // private icon/asset holder
_Label/Direction // documentation annotation helper---
2. Creating the Component Page
Each component lives on its own dedicated page (one page per component is the default). The page contains: a documentation frame at top-left and the component set positioned to its right or below.
// Create or find the component page
let page = figma.root.children.find(p => p.name === 'Button');
if (!page) {
page = figma.createPage();
page.name = 'Button';
}
await figma.setCurrentPageAsync(page);
// Documentation frame — positioned at (40, 40)
const docFrame = figma.createFrame();
docFrame.name = 'Button / Documentation';
docFrame.x = 40;
docFrame.y = 40;
docFrame.resize(600, 400);
docFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
docFrame.layoutMode = 'VERTICAL';
docFrame.primaryAxisSizingMode = 'AUTO';
docFrame.counterAxisSizingMode = 'FIXED';
docFrame.paddingTop = 40;
docFrame.paddingBottom = 40;
docFrame.paddingLeft = 40;
docFrame.paddingRight = 40;
docFrame.itemSpacing = 16;
// Title text node
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
const title = figma.createText();
title.fontName = { family: 'Inter', style: 'Bold' };
title.fontSize = 32;
title.characters = 'Button';
docFrame.appendChild(title);
// Description text node
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
const desc = figma.createText();
desc.fontName = { family: 'Inter', style: 'Regular' };
desc.fontSize = 14;
desc.characters = 'Buttons allow users to take actions and make choices with a single tap.';
docFrame.appendChild(desc);
// Tag docFrame with sharedPluginData for idempotency
docFrame.setSharedPluginData('dsb', 'run_id', RUN_ID);
docFrame.setSharedPluginData('dsb', 'key', 'doc/button');
return { docFrameId: docFrame.id, pageId: page.id };---
3. Base Component: Auto-Layout, Child Nodes, Variable Bindings
The base component is the template from which all variants are cloned. It must have: 1. Auto-layout (not manual positioning) 2. All child nodes present 3. ALL visual properties bound to variables (no hardcoded values)
Complete Button Base Component Example
const RUN_ID = 'ds-build-2024-001'; // replace with your actual run ID
await figma.setCurrentPageAsync(
figma.root.children.find(p => p.name === 'Button')
);
// Rehydrate variables from IDs stored in state ledger
const bgVar = await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_primary');
const textVar = await figma.variables.getVariableByIdAsync('VAR_ID_color_text_on_primary');
const paddingVar = await figma.variables.getVariableByIdAsync('VAR_ID_spacing_md');
const radiusVar = await figma.variables.getVariableByIdAsync('VAR_ID_radius_md');
const gapVar = await figma.variables.getVariableByIdAsync('VAR_ID_spacing_sm');
// --- Base component frame ---
const comp = figma.createComponent();
comp.name = 'Size=Medium, Style=Primary, State=Default';
comp.layoutMode = 'HORIZONTAL';
comp.primaryAxisSizingMode = 'AUTO';
comp.counterAxisSizingMode = 'AUTO';
comp.counterAxisAlignItems = 'CENTER';
comp.primaryAxisAlignItems = 'CENTER';
// Padding — bound to spacing variables
comp.setBoundVariable('paddingTop', paddingVar);
comp.setBoundVariable('paddingBottom', paddingVar);
comp.setBoundVariable('paddingLeft', paddingVar);
comp.setBoundVariable('paddingRight', paddingVar);
comp.setBoundVariable('itemSpacing', gapVar);
// Corner radius — bound to radius variable
comp.setBoundVariable('topLeftRadius', radiusVar);
comp.setBoundVariable('topRightRadius', radiusVar);
comp.setBoundVariable('bottomLeftRadius', radiusVar);
comp.setBoundVariable('bottomRightRadius', radiusVar);
// Background fill — bound to color variable
const bgPaint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } },
'color',
bgVar
);
comp.fills = [bgPaint];
// --- Label text node ---
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
const label = figma.createText();
label.name = 'label';
label.fontName = { family: 'Inter', style: 'Medium' };
label.fontSize = 14;
label.characters = 'Button';
label.layoutSizingHorizontal = 'HUG';
label.layoutSizingVertical = 'HUG';
// Text fill — bound to color variable
const textPaint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } },
'color',
textVar
);
label.fills = [textPaint];
comp.appendChild(label);
// --- Icon placeholder (Rectangle for now — will be INSTANCE_SWAP) ---
const iconBox = figma.createFrame();
iconBox.name = 'icon';
iconBox.resize(16, 16);
iconBox.fills = [];
iconBox.layoutSizingHorizontal = 'FIXED';
iconBox.layoutSizingVertical = 'FIXED';
comp.appendChild(iconBox);
// Tag for idempotency
comp.setSharedPluginData('dsb', 'run_id', RUN_ID);
comp.setSharedPluginData('dsb', 'phase', 'phase3');
comp.setSharedPluginData('dsb', 'key', 'component/button/base');
return { baseCompId: comp.id };ALL of these must be variable-bound (never hardcoded):
| Property | Variable type | API method |
|---|---|---|
| Fill color | COLOR | setBoundVariableForPaint(..., 'color', var) |
| Stroke color | COLOR | setBoundVariableForPaint(..., 'color', var) |
| Text fill | COLOR | setBoundVariableForPaint(..., 'color', var) |
| Padding (all 4 sides) | FLOAT | comp.setBoundVariable('paddingTop', var) |
| Gap / itemSpacing | FLOAT | comp.setBoundVariable('itemSpacing', var) |
| Corner radius (all 4) | FLOAT | comp.setBoundVariable('topLeftRadius', var) etc. |
| Stroke weight | FLOAT | comp.setBoundVariable('strokeWeight', var) |
---
4. Variant Matrix
Defining Axes
For each component, identify its variant axes before writing any code. Standard axes:
Button:
Size → [Small, Medium, Large]
Style → [Primary, Secondary, Outline, Ghost]
State → [Default, Hover, Focused, Pressed, Disabled]
Total = 3 × 4 × 5 = 60 combinations — exceeds 30 limit → split by StyleThe 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.
const RUN_ID = 'ds-build-2024-001';
const BASE_COMP_ID = 'BASE_ID_FROM_STATE'; // from state ledger
await figma.setCurrentPageAsync(
figma.root.children.find(p => p.name === 'Button')
);
const base = await figma.getNodeByIdAsync(BASE_COMP_ID);
// Variable IDs from state ledger
const vars = {
// Primary style
bg_primary: await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_primary'),
text_primary: await figma.variables.getVariableByIdAsync('VAR_ID_color_text_on_primary'),
// Secondary style
bg_secondary: await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_secondary'),
text_secondary: await figma.variables.getVariableByIdAsync('VAR_ID_color_text_secondary'),
// Disabled
bg_disabled: await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_disabled'),
text_disabled: await figma.variables.getVariableByIdAsync('VAR_ID_color_text_disabled'),
// Sizes
padding_sm: await figma.variables.getVariableByIdAsync('VAR_ID_spacing_sm'),
padding_md: await figma.variables.getVariableByIdAsync('VAR_ID_spacing_md'),
padding_lg: await figma.variables.getVariableByIdAsync('VAR_ID_spacing_lg'),
};
const axes = {
Size: ['Small', 'Medium', 'Large'],
Style: ['Primary', 'Secondary'],
State: ['Default', 'Hover', 'Disabled'],
};
const paddingBySize = { Small: vars.padding_sm, Medium: vars.padding_md, Large: vars.padding_lg };
const components = [];
for (const size of axes.Size) {
for (const style of axes.Style) {
for (const state of axes.State) {
const clone = base.clone();
clone.name = `Size=${size}, Style=${style}, State=${state}`;
// Bind padding by size
clone.setBoundVariable('paddingTop', paddingBySize[size]);
clone.setBoundVariable('paddingBottom', paddingBySize[size]);
clone.setBoundVariable('paddingLeft', paddingBySize[size]);
clone.setBoundVariable('paddingRight', paddingBySize[size]);
// Bind fill by style + state
const isDisabled = state === 'Disabled';
const bgVar = isDisabled ? vars.bg_disabled : (style === 'Primary' ? vars.bg_primary : vars.bg_secondary);
const txtVar = isDisabled ? vars.text_disabled : (style === 'Primary' ? vars.text_primary : vars.text_secondary);
const bgPaint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', bgVar
);
clone.fills = [bgPaint];
const labelNode = clone.findOne(n => n.name === 'label');
const textPaint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }, 'color', txtVar
);
labelNode.fills = [textPaint];
clone.setSharedPluginData('dsb', 'run_id', RUN_ID);
clone.setSharedPluginData('dsb', 'key', `component/button/variant/${size}/${style}/${state}`);
components.push(clone);
}
}
}
return { variantIds: components.map(c => c.id) };---
5. combineAsVariants + Grid Layout
After all variant components exist, combine them into a ComponentSet and position them in a grid. This MUST be a separate use_figma call — you must pass in all variant IDs from the previous call's return value.
Grid Design Conventions
Professional design systems lay out variants in a readable grid where:
- Columns = the property users interact with most (typically State: Default, Hover, Focused, Pressed, Disabled)
- Rows = structural axes grouped together (typically Size × Style, where Size varies fastest)
- Gap = 16–40px between variants (20px is a safe default; match existing file if one exists)
- Padding = 40px around the grid inside the ComponentSet frame
Visual structure:
Default Hover Focused Pressed Disabled
┌──────────────────────────────────────────────────────────────────┐
│ Small/Primary [comp] [comp] [comp] [comp] [comp] │
│ Small/Secondary [comp] [comp] [comp] [comp] [comp] │
│ Medium/Primary [comp] [comp] [comp] [comp] [comp] │
│ Medium/Secondary[comp] [comp] [comp] [comp] [comp] │
│ Large/Primary [comp] [comp] [comp] [comp] [comp] │
│ Large/Secondary [comp] [comp] [comp] [comp] [comp] │
└──────────────────────────────────────────────────────────────────┘Why State on columns? State is the axis designers scan horizontally to verify interaction consistency. Size/Style define the "identity" of each row. This matches how professional design systems (M3, Polaris, Simple DS) organize their grids.
Adding Row/Column Header Labels
After laying out the grid, add text labels OUTSIDE the ComponentSet to help navigation. These are siblings of the ComponentSet on the page — not children of it:
// 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
const VARIANT_IDS = ['ID1', 'ID2', '...']; // from state ledger
const PAGE_ID = 'PAGE_ID'; // from state ledger
await figma.setCurrentPageAsync(await figma.getNodeByIdAsync(PAGE_ID));
// Collect component nodes
const components = await Promise.all(
VARIANT_IDS.map(id => figma.getNodeByIdAsync(id))
);
// Combine as variants
const cs = figma.combineAsVariants(components, figma.currentPage);
cs.name = 'Button';
// Grid layout: position each variant based on its property values
// Determine column axis (State) and row axes (Size × Style)
const axes = {
Size: ['Small', 'Medium', 'Large'],
Style: ['Primary', 'Secondary'],
State: ['Default', 'Hover', 'Disabled'],
};
const COL_AXIS = 'State'; // columns
const ROW_AXES = ['Size', 'Style']; // rows (Size changes fastest)
const gap = 16;
const padding = 40;
// Measure child dimensions (all should be same height within Size tier)
// Use the first child as reference for column width
const childWidth = 120; // approximate; refine after first screenshot
const childHeight = 40;
cs.children.forEach(child => {
const props = {};
child.name.split(', ').forEach(part => {
const [k, v] = part.split('=');
props[k] = v;
});
const colIdx = axes[COL_AXIS].indexOf(props[COL_AXIS]);
// Row = Size index * number of styles + Style index
const rowIdx = axes.Size.indexOf(props.Size) * axes.Style.length
+ axes.Style.indexOf(props.Style);
child.x = padding + colIdx * (childWidth + gap);
child.y = padding + rowIdx * (childHeight + gap);
});
// Resize component set to fit all children + padding
let maxX = 0, maxY = 0;
for (const child of cs.children) {
maxX = Math.max(maxX, child.x + child.width);
maxY = Math.max(maxY, child.y + child.height);
}
cs.resizeWithoutConstraints(maxX + padding, maxY + padding);
// Style the component set frame
cs.fills = [{ type: 'SOLID', color: { r: 0.95, g: 0.95, b: 0.98 } }];
cs.cornerRadius = 8;
// Position component set on page (to the right of doc frame)
cs.x = 680;
cs.y = 40;
cs.setSharedPluginData('dsb', 'run_id', 'ds-build-2024-001');
cs.setSharedPluginData('dsb', 'key', 'componentset/button');
return { componentSetId: cs.id };Critical rules for combineAsVariants:
componentsmust be a non-empty array containing ONLYComponentNodeobjects (not frames, not groups)- After combining, children are placed at (0,0) and overlap — you MUST manually position them
resizeWithoutConstraintsis 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:
// On the ComponentSetNode (cs):
const labelKey = cs.addComponentProperty('Label', 'TEXT', 'Button');
// labelKey is now something like "Label#0:1"
// Wire to the label child in each variant:
for (const child of cs.children) {
const labelNode = child.findOne(n => n.name === 'label');
if (labelNode) {
labelNode.componentPropertyReferences = { characters: labelKey };
}
}BOOLEAN Properties
Toggle child node visibility:
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):
// 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:
// 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];
}
});
iconComp.setSharedPluginData('dsb', 'run_id', RUN_ID);
iconComp.setSharedPluginData('dsb', 'key', 'icon/chevron-right');
return { iconCompId: iconComp.id };Then use the returned `iconCompId` as the default value for INSTANCE_SWAP:
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:
// Get the property definitions to find the exact key
const props = cs.componentPropertyDefinitions;
const iconPropKey = Object.keys(props).find(k => k.startsWith('Icon'));
// Set preferred values (array of component keys or instance IDs)
cs.editComponentProperty(iconPropKey, {
preferredValues: [
{ type: 'COMPONENT', key: chevronRightComp.key },
{ type: 'COMPONENT', key: chevronLeftComp.key },
{ type: 'COMPONENT', key: closeComp.key },
],
});Icon library tip: Create all icon components on a dedicated Icons page before building any UI components. Then reference their IDs when wiring INSTANCE_SWAP properties.
componentPropertyReferences mapping
The componentPropertyReferences object maps a node's own property to a component property key:
| Node property | Component property type | Used for |
|---|---|---|
characters | TEXT | Editable text content |
visible | BOOLEAN | Show/hide toggle |
mainComponent | INSTANCE_SWAP | Swap nested instances |
---
7. sharedPluginData Tagging for Idempotency
Tag EVERY created node immediately after creation. This enables safe cleanup, resumability, and idempotency checks.
// After creating any node:
node.setSharedPluginData('dsb', 'run_id', RUN_ID); // identifies the build run
node.setSharedPluginData('dsb', 'phase', 'phase3'); // which phase created it
node.setSharedPluginData('dsb', 'key', KEY); // unique logical key for this entity
// Reading back:
const runId = node.getSharedPluginData('dsb', 'run_id'); // '' if not set
const key = node.getSharedPluginData('dsb', 'key');Key naming convention: use /-separated logical paths that mirror the entity hierarchy:
'component/button/base'
'component/button/variant/Medium/Primary/Default'
'componentset/button'
'doc/button'
'page/button'Idempotency check before creating: before creating a node, scan the current page for an existing node with the same key:
// Indexed sharedPluginData lookup — the engine only visits nodes that
// actually carry the dsb namespace key, not every node on the page.
const existing = figma.currentPage
.findAllWithCriteria({ sharedPluginData: { namespace: 'dsb', keys: ['key'] } })
.filter(n => n.getSharedPluginData('dsb', 'key') === 'componentset/button');
if (existing.length > 0) {
// Skip creation — already done. Return existing node's ID.
return { componentSetId: existing[0].id };
}---
8. Documentation
Page title + description frame
The documentation frame (see Section 2) should contain: 1. Component name as a large title (32px+ Bold) 2. 1–3 sentence description of what the component is and when to use it 3. Spec notes (sizes, spacing values, accessibility notes)
Component description property
Set the description on the ComponentSet — it appears in the Figma properties panel and is exported as documentation:
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):
cs.documentationLinks = [
{ uri: 'https://your-storybook.com/button' }
];Node names and organization
- ComponentSet: plain component name —
'Button' - Individual variants:
'Property=Value, Property=Value'format (match the file's existing casing) - Child nodes: semantic names —
'label','icon','container','state-layer' - Documentation frames:
'ComponentName / Documentation'
---
9. Validation
Always validate after creating or modifying a component before proceeding to the next one.
get_metadata structural checks
After creating the component set, call get_metadata on the ComponentSet node and verify:
variantGroupPropertieslists the expected axes with the correct value arrayscomponentPropertyDefinitionscontains the expected TEXT/BOOLEAN/INSTANCE_SWAP propertieschildren.lengthequals the expected variant count (e.g., 18 for 3×2×3)- No children are named
'Component 1'(unnamed components are a sign of a bug)
get_screenshot — Visual Validation (Critical)
get_screenshot returns an image of the specified node. Call it on the component page node (not the component set) to see the full page including documentation and grid labels.
Tool: get_screenshot
Args: { nodeId: "PAGE_NODE_ID", fileKey: "FILE_KEY" }How to use the screenshot:
1. Display it to the user — this is the primary purpose. Show the screenshot as part of the user checkpoint: "Here's the Button component. Does it look right?" 2. Analyze it yourself — if you have vision capabilities, check the visual checklist below. If you don't (text-only agent), fall back to structural validation only via get_metadata and describe what you created textually.
Visual validation checklist (check each item when viewing the screenshot):
| # | Check | What "good" looks like | What "broken" looks like |
|---|---|---|---|
| 1 | Grid layout | Variants in neat rows and columns with consistent spacing | All variants piled at top-left (0,0 stacking bug) |
| 2 | Color fills | Components show distinct, correct colors per style variant | All components are black or same color (variable binding failed) |
| 3 | Size differentiation | Small variants are visibly smaller than Large variants | All variants are the same size (height/padding not bound to variables) |
| 4 | Text readability | Labels are visible with correct font and color | Text is invisible (white on white), missing, or shows "undefined" |
| 5 | Spacing/padding | Interior padding visible, components aren't "shrink-wrapped" | Components look cramped or have no visible internal space |
| 6 | State differentiation | Hover/Pressed variants have visible color differences from Default | All states look identical (state-specific fills not applied) |
| 7 | Disabled state | Lower opacity or muted colors compared to active states | Disabled looks identical to Default |
| 8 | Documentation frame | Title + description text visible above or beside the component grid | No documentation, or it overlaps the component set |
| 9 | Grid labels | Row/column headers visible around the component set (if added) | Labels overlap the grid or are missing |
| 10 | Component set boundary | Gray background frame wraps all variants with even padding | Frame is too small (variants clipped) or way too large |
Screenshot → diagnosis → fix mapping:
| Screenshot shows | Diagnosis | Fix script |
|---|---|---|
| All variants stacked top-left | Grid layout wasn't applied after combineAsVariants | Re-run the grid layout script (§5) |
| Everything black/same color | Variable bindings failed or variables don't have values for the active mode | Re-run variable binding, check mode values |
| No text visible | Font wasn't loaded, or text fill is same color as background | 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 |
| 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:
const cs = await figma.getNodeByIdAsync(CS_ID);
const sample = cs.children.slice(0, 3).map(c => ({
name: c.name,
width: c.width, height: c.height,
x: c.x, y: c.y,
fills: c.fills?.map(f => f.type === 'SOLID' ?
{ r: f.color.r.toFixed(2), g: f.color.g.toFixed(2), b: f.color.b.toFixed(2), boundVar: f.boundVariables?.color?.id } : f.type
),
}));
return { sampleVariants: sample, totalChildren: cs.children.length };This gives you positions (grid working?), dimensions (size differentiation?), and fill info (bindings working?) without needing vision.
When to take a screenshot:
- After EVERY completed component (mandatory — part of the user checkpoint)
- After creating the foundations documentation page
- After final QA (screenshot every page)
- Do NOT screenshot after every intermediate step (wastes tool calls)
Common issues
| Symptom | Likely cause | Fix |
|---|---|---|
| All variants stacked at (0,0) | combineAsVariants was called but children were never repositioned | Re-run grid layout script |
| Variants show wrong colors | Variable bindings applied after combineAsVariants instead of before | Rebind on component set children |
| Variant count wrong | Clone loop indexing error | Print components.map(c => c.name) before combining |
| BOOLEAN property has no effect | componentPropertyReferences was set on the component set frame, not on the child node | Find the actual child node and set references there |
| INSTANCE_SWAP shows no swap option | Default value was not a valid component ID | Pass a real existing component ID as defaultValue |
combineAsVariants throws | At least one node in the array is not a ComponentNode | Filter array: nodes.filter(n => n.type === 'COMPONENT') |
addComponentProperty returns unexpected key | Expected — the key gets a #id:id suffix | Save the returned value immediately: const key = cs.addComponentProperty(...) |
---
10. Complete Worked Example: Button Component
This shows the full sequence of use_figma calls for a Button component, including state passing between calls. Replace RUN_ID and variable IDs with your actual values from the state ledger.
Call 1: Create the component page
Goal: Create (or find) the Button page. State input: None State output: { pageId }
let page = figma.root.children.find(p => p.name === 'Button');
if (!page) { page = figma.createPage(); page.name = 'Button'; }
page.setSharedPluginData('dsb', 'run_id', 'ds-build-2024-001');
page.setSharedPluginData('dsb', 'key', 'page/button');
return { pageId: page.id };Call 2: Create documentation frame
Goal: Add title + description frame. State input: { pageId } State output: { docFrameId }
const PAGE_ID = 'PAGE_ID_FROM_STATE';
const page = await figma.getNodeByIdAsync(PAGE_ID);
await figma.setCurrentPageAsync(page);
// Idempotency check — use the sharedPluginData index instead of a per-node
// findAll callback.
const existing = page
.findAllWithCriteria({ sharedPluginData: { namespace: 'dsb', keys: ['key'] } })
.filter(n => n.getSharedPluginData('dsb', 'key') === 'doc/button');
if (existing.length > 0) {
return { docFrameId: existing[0].id };
}
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
const docFrame = figma.createFrame();
docFrame.name = 'Button / Documentation';
docFrame.x = 40; docFrame.y = 40;
docFrame.layoutMode = 'VERTICAL';
docFrame.primaryAxisSizingMode = 'AUTO';
docFrame.counterAxisSizingMode = 'FIXED';
docFrame.resize(560, 100);
docFrame.paddingTop = 40; docFrame.paddingBottom = 40;
docFrame.paddingLeft = 40; docFrame.paddingRight = 40;
docFrame.itemSpacing = 16;
docFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
const title = figma.createText();
title.fontName = { family: 'Inter', style: 'Bold' };
title.fontSize = 32;
title.characters = 'Button';
docFrame.appendChild(title);
const desc = figma.createText();
desc.fontName = { family: 'Inter', style: 'Regular' };
desc.fontSize = 14;
desc.characters = 'Buttons allow users to take actions with a single tap. Use Primary for the highest-emphasis action on a page, Secondary for supporting actions.';
desc.layoutSizingHorizontal = 'FILL';
docFrame.appendChild(desc);
docFrame.setSharedPluginData('dsb', 'run_id', 'ds-build-2024-001');
docFrame.setSharedPluginData('dsb', 'key', 'doc/button');
return { docFrameId: docFrame.id };Call 3: Create base component
Goal: Create the base component with auto-layout and all variable bindings. State input: { pageId } + variable IDs from Phase 1 State output: { baseCompId }
(See Section 3 for full code — substituting the actual variable IDs from the state ledger.)
Call 4: Create all variants
Goal: Clone base and produce all 18 variants (3 Size × 2 Style × 3 State). State input: { pageId, baseCompId } + variable IDs State output: { variantIds: ['id1', 'id2', ..., 'id18'] }
const RUN_ID = 'ds-build-2024-001';
const BASE_ID = 'BASE_COMP_ID_FROM_STATE';
const PAGE_ID = 'PAGE_ID_FROM_STATE';
// Variable IDs from state ledger:
const VAR = {
bg_primary: 'VAR_ID_1',
text_primary: 'VAR_ID_2',
bg_secondary: 'VAR_ID_3',
text_secondary: 'VAR_ID_4',
bg_disabled: 'VAR_ID_5',
text_disabled: 'VAR_ID_6',
padding_sm: 'VAR_ID_7',
padding_md: 'VAR_ID_8',
padding_lg: 'VAR_ID_9',
};
const page = await figma.getNodeByIdAsync(PAGE_ID);
await figma.setCurrentPageAsync(page);
const base = await figma.getNodeByIdAsync(BASE_ID);
// Load all variables 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
)];
clone.setSharedPluginData('dsb', 'run_id', RUN_ID);
clone.setSharedPluginData('dsb', 'key', `component/button/variant/${size}/${style}/${state}`);
components.push(clone);
}
}
}
return { variantIds: components.map(c => c.id) };Call 5: combineAsVariants + grid layout
Goal: Combine all 18 variants into a ComponentSet and lay them out in a grid. State input: { pageId, variantIds } (18 IDs) State output: { componentSetId }
(See Section 5 for full code.)
Call 6: Add component properties
Goal: Add TEXT, BOOLEAN, INSTANCE_SWAP properties and wire them to child nodes. State input: { pageId, componentSetId } State output: { componentSetId, properties: { labelKey, showIconKey, iconKey } }
const CS_ID = 'CS_ID_FROM_STATE';
const DEFAULT_ICON_ID = 'ICON_COMP_ID_FROM_STATE';
const page = figma.root.children.find(p => p.name === 'Button');
await figma.setCurrentPageAsync(page);
const cs = await figma.getNodeByIdAsync(CS_ID);
cs.description = 'Buttons allow users to take actions and make choices with a single tap.';
cs.documentationLinks = [{ uri: 'https://your-storybook.com/button' }];
// Add properties — save returned keys
const labelKey = cs.addComponentProperty('Label', 'TEXT', 'Button');
const showIconKey = cs.addComponentProperty('Show Icon', 'BOOLEAN', true);
const iconKey = cs.addComponentProperty('Icon', 'INSTANCE_SWAP', DEFAULT_ICON_ID);
// Wire to children
for (const child of cs.children) {
const labelNode = child.findOne(n => n.name === 'label');
if (labelNode) labelNode.componentPropertyReferences = { characters: labelKey };
const iconNode = child.findOne(n => n.name === 'icon');
if (iconNode) {
iconNode.componentPropertyReferences = {
visible: showIconKey,
...(iconNode.type === 'INSTANCE' ? { mainComponent: iconKey } : {}),
};
}
}
return {
componentSetId: cs.id,
properties: { labelKey, showIconKey, iconKey },
};Call 7: Validate with get_metadata
Goal: Structural check — variant count, properties, axes. Action: Call get_metadata on the ComponentSet node ID (from state). Verify in the result:
children.length === 18variantGroupPropertieshasSize,Style,Statekeys with correct value arrayscomponentPropertyDefinitionshasLabel,Show Icon,Iconentries
Call 8: Validate with get_screenshot
Goal: Visual check — layout, colors, text. Action: Call get_screenshot on the Button page. Inspect the screenshot. If variants are stacked, re-run Call 5. If colors look wrong, inspect variable bindings.
Checkpoint
After Call 8: show the screenshot to the user. Ask: "Here's the Button component with 18 variants. Does this look correct?" Do not proceed to the next component until the user approves.
Part of the figma-generate-library skill.
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`:
// 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 = 16Tailwind 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.
{
"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
// 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 collectioniOS Token Sources
// Asset catalog colors in .xcassets/Colors.xcassets
// extension Color { static let bgPrimary = Color("bg-primary") }
// Look for traitCollection.userInterfaceStyle for dark mode detectionAndroid Token Sources
// 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.
/* 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
// 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 at the start of every build. All are read-only and safe to run before any user checkpoint.
List All Pages
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
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)
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
// Read-only inspection — skip invisible instance interiors for speed.
figma.skipInvisibleInstanceChildren = true;
// 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.
List All Styles
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
// 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,
sharedPluginData: node.getSharedPluginData('dsb', 'key') || null
};---
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 pageReturns:
- `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
offsetto 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 runs three parallel searches against design libraries for the given file:
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({
query: "button", // required — text query
fileKey: "abc123", // required — your file key
includeComponents: true, // default true
includeVariables: true, // default true
includeStyles: true // default true
})
// Search a specific library only (use libraryKey from get_libraries)
search_design_system({
query: "button",
fileKey: "abc123",
includeLibraryKeys: ["lk-abc123..."],
includeComponents: true
})What It Returns
{
"components": [
{
"name": "Button",
"libraryName": "Design System",
"assetType": "component_set",
"componentKey": "abc123def",
"description": "Primary action button"
}
],
"variables": [
{
"name": "colors/primary/500",
"variableType": "COLOR",
"variableSetKey": "set1key",
"key": "var1key",
"scopes": ["FRAME_FILL", "SHAPE_FILL"],
"variableCollectionName": "Colors"
}
],
"styles": [
{
"name": "Heading/H1",
"styleType": "TEXT",
"key": "style1key"
}
]
}How to Interpret Results
Components: The componentKey can be used in use_figma to import the component:
const component = await figma.importComponentByKeyAsync("abc123def");
// or for component sets:
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
- Phase 0, step 0c: Search broadly (
query: "button",query: "color",query: "spacing") before planning anything. This establishes the reuse baseline. - Immediately before each component creation: Search for the specific component name before writing any
use_figmacreation code.
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 Checkpoint Message Template
Present this message before proceeding. Never begin Phase 1 without explicit user approval.
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}
Libraries to search before each component: {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, animation timing)
Shall I proceed?---
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?"
Part of the figma-generate-library skill.
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 useInterbecause it's available everywhere, butloadFontAsyncis required for every (family, style) pair you mutate — not just Inter.
Design files only. Every snippet here (includingfigma.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
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
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
/**
* 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 } }];
label.layoutSizingHorizontal = 'FILL';
swatchFrame.appendChild(label);
// 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 } }];
pathLabel.layoutSizingHorizontal = 'FILL';
swatchFrame.appendChild(pathLabel);
parent.appendChild(swatchFrame);
return swatchFrame;
}Color section builder (primitives row + semantic grid)
/**
* 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 } }];
desc.layoutSizingHorizontal = 'FILL';
section.appendChild(desc);
// 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
/**
* 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 } }];
nameText.layoutSizingHorizontal = 'FILL';
row.appendChild(nameText);
// 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 } }];
specimen.layoutSizingHorizontal = 'FILL';
row.appendChild(specimen);
// 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 } }];
specs.layoutSizingHorizontal = 'FILL';
row.appendChild(specs);
// Divider line
const divider = figma.createRectangle();
divider.resize(1280, 1);
divider.fills = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }];
divider.layoutSizingHorizontal = 'FILL';
row.appendChild(divider);
return row;
}Typography section builder
/**
* 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
/**
* 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
/**
* 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
/**
* 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
/**
* 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
/**
* 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
/**
* 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
/**
* 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 } }];
title.layoutSizingHorizontal = 'FILL';
doc.appendChild(title);
// 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 } }];
descText.layoutSizingHorizontal = 'FILL';
doc.appendChild(descText);
// Divider
const divider = figma.createRectangle();
divider.resize(280, 1);
divider.fills = [{ type: 'SOLID', color: { r: 0.88, g: 0.88, b: 0.88 } }];
divider.layoutSizingHorizontal = 'FILL';
doc.appendChild(divider);
// 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 } }];
noteText.layoutSizingHorizontal = 'FILL';
doc.appendChild(noteText);
}
}
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.
/**
* 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 }
}
/**
* 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.
*
* Every created collection is tagged with `dsb_key` plugin data so it can be
* found and cleaned up idempotently by `cleanupOrphans`.
*
* @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"]).
* @param {string} [runId] - Optional dsb_run_id to tag for cleanup.
* @returns {Promise<{
* collection: VariableCollection,
* modeIds: Record<string, string>
* }>}
* `modeIds` maps each mode name to its modeId string.
*/
async function createVariableCollection(name, modeNames, runId) {
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)
// Tag for idempotent cleanup
collection.setPluginData('dsb_key', `collection/${name}`)
if (runId) {
collection.setPluginData('dsb_run_id', runId)
}
// 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 }
}
Related skills
Forks & variants (2)
Figma Generate Library has 2 known copies in the catalog totaling 1.6k installs. They canonicalize to this original listing.
How it compares
Pick figma-generate-library over figma-use alone when building full token foundations and component libraries—not single ad-hoc Plugin API scripts.
FAQ
Can I create all components in one call?
No. One-shot builds break structure; build one component per use_figma call with checkpoints.
What comes before components?
Variable collections, primitives, semantics, scopes, and code syntax on all tokens.
Which companion skill is required?
Load figma-use alongside for Plugin API syntax on every use_figma call.
Is Figma Generate Library safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.