figma-generate-library
by Figma · official repository
Build or update a professional-grade design system in Figma from a codebase. Use when the user wants to create variables/tokens, build component libraries, create individual components with proper variant sets and variable bindings, set up theming (light/dark modes), document foundations, or reconcile gaps between code and Figma. Also use when the user asks to create or generate any component in Figma — even a single one — since components require proper variable foundations, variant states, and design token bindings to be production-quality. This skill teaches WHAT to build and in WHAT ORDER — it complements the `figma-use` skill which teaches HOW to call the Plugin API. Both skills should be loaded together.
Labels from its files: runs commands. 16 files, read Oct 21, 2026. Nothing was run.
npx skills add https://github.com/figma/mcp-server-guide --skill figma-generate-libraryWhat it lets an agent do
Each label, and the line in the skill's files that set it.
- Runs commands
Allows a shell or ships scripts the agent runs.
- It ships 8 scripts the agent can run.
Findings
Matches of fixed checks, with the text they matched.
No check matched anything in this skill's files.
Not a safety rating, and no finding is not a safety guarantee. These are fixed checks of the files as read on Oct 21, 2026. Nothing was run. How skills are checked.
SKILL.md
As the publisher wrote it. Shown as text, never rendered.
---
name: figma-generate-library
description: "Build or update a professional-grade design system in Figma from a codebase. Use when the user wants to create variables/tokens, build component libraries, create individual components with proper variant sets and variable bindings, set up theming (light/dark modes), document foundations, or reconcile gaps between code and Figma. Also use when the user asks to create or generate any component in Figma — even a single one — since components require proper variable foundations, variant states, and design token bindings to be production-quality. This skill teaches WHAT to build and in WHAT ORDER — it complements the `figma-use` skill which teaches HOW to call the Plugin API. Both skills should be loaded together."
disable-model-invocation: false
---
# Design System Builder — Figma MCP Skill
Build professional-grade design 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
For every phase, follow this communication contract.
Before starting a phase:
- Post a user-facing checklist titled `Phase N Checklist`.
- Include every task/subtask that will be attempted in that phase.
- Include the phase exit criteria.
- Do not begin mutating work for the phase until this checklist has been posted.
During execution:
- Before each major subsection, post a short update naming the exact section being worked on, using this format:
`Working on Phase N.X: <section name>`
- Keep updates concise, but make the current work visible.
- When a subsection completes, mark it as completed in the running checklist if the interface supports checklist/status updates; otherwise mention completion in the next progress update.
At the end of each phase:
- Post a `Phase N Summary` with:
- Completed tasks
- Created or changed Figma objects
- Validations performed
- Decisions or conflicts resolved
- Remaining risks or follow-ups
- Then show the required phase artifact for that phase and continue automatically.
### Stable Task IDs
Use one task ID format everywhere: `P{phase}.{step}`.
Rules:
- Use lettered step IDs only: `P0.a`, `P0.b`, `P1.a`, `P3.d`.
- Do not use plain bullet points for task lists.
- Every phase checklist, progress update, validation note, and phase summary MUST reference the same task IDs
**No setup exception:** Creating a new Figma file, importing a library, creating pages, variables, collections, styles, or components all count as creation/mutation. Do not treat any of them as harmless setup.
**This is NEVER a one-shot task.** Building a design system requires 20–100+ `use_figma` calls across multiple phases, with mandatory progress 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
Work through the phases in order. Do not move to the next phase until the current phase's required actions and acceptance checks are complete. If a phase cannot pass, stop and report the blocker. Do not approximate, skip, or defer a failed phase unless the user explicitly approves the limitation. No best-effort substitutions. No quiet approximations. No handoff with missing source truth, missing visual truth, fake assets, approximate typography, broken interactions, or unverified states.
### 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. Discover and search libraries → call `get_libraries` for the target file before `search_design_system`
- [ ] 0d. Lock v1 scope → exact token set + component list recorded before any creation
- [ ] 0e. Map code → Figma → every conflict (code disagrees with Figma) resolved and recorded
- [ ] 0f. Print a **gap analysis** to chat: what exists in code but not Figma, what exists in Figma but not code, and every conflict from 0e with its resolution
### 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 (never `ALL_SCOPES`)
- [ ] 1e. Set code syntax on ALL variables
- [ ] 1f. Create effect styles (shadows) and text styles (typography)
- [ ] 1g. Print a **variable summary** to chat: N collections, M variables, K modes, broken down by collection
- [ ] 1h. Print the **style list** to chat: every effect style and text style created, with names
- [ ] Exit criteria met: every token from the agreed plan exists, all scopes set, all code syntax set
### 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)
- [ ] 2c. Capture a `get_screenshot` of every foundations page and print the **page list** to chat alongside the screenshots
- [ ] Exit criteria met: all planned pages exist, foundations docs are navigable
### Phase 3: COMPONENTS (one at a time — never batch)
For EACH component (in dependency order: atoms before molecules), run the checklist below. Finish the current component before starting the next.
- [ ] 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 met: variant count correct, all bindings verified, screenshot looks right
### 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
---
## 3. Critical Rules
**Plugin API basics** (from use_figma skill — enforced here too):
- Use `return` to send data back (auto-serialized). Do NOT wrap in IIFE or call closePlugin.
- Return ALL created/mutated node IDs in every return value
- Page context resets each call — always `await figma.setCurrentPageAsync(page)` at start. **Call it at most once per script**: each component or doc page is its own `use_figma` call. Never loop over `figma.root.children` and switch pages inside a mutating script — split that work into one focused call per target page (see [figma-use → gotchas.md → Set current page once per `use_figma` call](../figma-use/references/gotchas.md#set-current-page-once-per-use_figma-call--split-multi-page-work-across-calls))
- `figma.notify()` throws — never use it
- Colors are 0–1 range, not 0–255
- Font MUST be loaded before any text write: `await figma.loadFontAsync({family, style})`. Use `await figma.listAvailableFontsAsync()` to discover available fonts and verify exact style strings — if a load fails, query available fonts to find the correct name or a fallback.
**Design system rules**:
1. **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. In `$fig`, bind by passing the variable handle straight into the property (`fills` color, `cornerRadius`, `itemSpacing`, padding); whenever a token exists for a value, prefer binding it over a literal ([worked recipe](../figma-use/references/fig-builder.md#building-a-component-with-bound-variables-the-default-for-components)). Because components are usually built in a **separate `use_figma` call** from the token foundations, rehydrate the variable IDs in the build call (`figma.variables.getVariableByIdAsync` / `$fig.getVar`) before binding — handles don't survive across calls, and skipping this is why a build silently falls back to literals. 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.
---
## 4. State Management (Required for Long Workflows)
> Do not store workflow state on Figma objects. Use deterministic names for discovery and exact returned IDs in the state ledger. Put human-readable component purpose and usage guidance in the component or component-set `description`.
| Entity type | Stable identity | How to check existence |
|-------------|----------------|----------------------|
| Pages and frames | Deterministic name + state-ledger ID | `figma.root.children.find(p => p.name === pageName)` or `await figma.getNodeByIdAsync(id)` |
| Components and component sets | Variant/set name + state-ledger ID | `page.findOne(n => n.name === name)` or `await figma.getNodeByIdAsync(id)` |
| Variables | Name within collection | `(await figma.variables.getLocalVariablesAsync()).find(v => v.name === name && v.variableCollectionId === collId)` |
| Styles | Name | `getLocalTextStyles().find(s => s.name === name)` |
Record every returned ID in the state ledger immediately after creation. Never use a fuzzy lookup to authorize deletion.
**State persistence**: Do NOT rely solely on conversation context for the state ledger. Write it to disk:
```
/tmp/design-system-state-{RUN_ID}.json
```
Re-read this file at the start of every turn. In long workflows, conversation context will be truncated — the file is the source of truth.
Maintain a state ledger tracking:
```json
{
"runId": "ds-build-2024-001",
"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.
Before calling `search_design_system` for a target file, you MUST call `get_libraries` first for that file. You MUST NOT assume libraries are added or available.
An empty `get_libraries` result does NOT excuse skipping the search — it only means you have no library keys to scope with. `get_libraries` paginates (community UI kits appear only on the first page, org libraries page in batches of 20), so empty lists are not proof that no library exists. How to act on the result:
- **Libraries returned** — run `search_design_system` scoped with `includeLibraryKeys`. Libraries in `libraries_available_to_add` are NOT searched by default; pass their `libraryKey`s to reach them.
- **No libraries returned** — still run `search_design_system`, but omit `includeLibraryKeys`. Omitting it scopes the search to the file itself, which is exactly what you want when discovery returned nothing to scope by.
Only once the search itself comes back empty may you record "no design system assets available" in the Phase 0f gap analysis and build from code tokens. Never infer "no libraries" from a failed or unattempted `get_libraries` call.
```
// Discover all libraries accessible to the file
get_libraries({ fileKey })
// Returns:
// libraries_added_to_file: [{ name, libraryKey, description, source }, ...]
// libraries_available_to_add: [{ name, libraryKey, description, source }, ...]
// libraries_available_to_add_next_offset: number | null
```
Use the returned `libraryKey` values to scope searches to specific libraries via `includeLibraryKeys`. This avoids noisy results when many libraries are available.
If `libraries_available_to_add_next_offset` is non-null, more org libraries are available — call `get_libraries` again with `offset` set to that value. Org libraries page in batches of 20; community UI kits only appear on the first page.
```
// Search across all libraries (default)
search_design_system({
queries: [
{ entity: "component", query },
{ entity: "variable", query },
{ entity: "style", query }
],
fileKey
})
// Search within a specific library only
search_design_system({ queries: [{ entity: "component", query }], fileKey, includeLibraryKeys: ["lk-abc123..."] })
```
**Reuse if** all of these are true:
- Component property API matches your needs (same variant axes, compatible types)
- Token binding model is compatible (uses same or aliasable variables)
- Naming conventions match the target file
- Component is editable (not locked in a remote library you don't own)
**Rebuild if** any of these:
- API incompatibility (different property names, wrong variant model)
- Token model incompatible (hardcoded values, different variable schema)
- Ownership issue (can't modify the library)
**Wrap if** visual match but API incompatible:
- Import the library component as a nested instance inside a new wrapper component
- Expose a clean API on the wrapper
**Priority order**: local existing → subscribed library import → unsubscribed UI Kit library from `libraries_available_to_add` (icons especially) → create new.
---
## 6. Decision Forks
Ask the user when paths fork — when two or more reasonable answers exist and no clear winner comes from the codebase, the Figma file, or the locked plan. Don't silently default. Present each option with its tradeoff and your recommendation; pick only after the user steers.
**When NOT to ask:** if exactly one path is clearly correct from the source of truth (code, Figma file, agreed plan), take it. This section is for genuine ambiguity, not for offloading every decision.
| Fork situation | What to surface | Example ask |
|---|---|---|
| Code ≠ Figma on a token, component, or value | Both versions side by side, with provenance (file/line vs node) | "Code says `--color-bg-primary = #FFFFFF`, Figma has `color/bg/primary = #FAFAFA`. Which wins?" |
| Subscribed library has a close-but-not-exact match | Library component summary + gap list | "Library has `Button` with no `loading` state. Reuse + wrap locally, or rebuild from scratch?" |
| Scope ambiguity at plan-lock (0d) | What's clearly in, what's clearly out, what's ambiguous | "Spec lists `Button` and `Input`; `Field` is referenced but not defined. In or out of v1?" |
**If the user rejects an option you already built on:** fix before moving on. Never build on rejected work.
---
## 7. Naming Conventions
Match existing file conventions. If starting fresh:
**Variables** (slash-separated):
```
color/bg/primary color/text/secondary color/border/default
spacing/xs spacing/sm spacing/md spacing/lg spacing/xl spacing/2xl
radius/none radius/sm radius/md radius/lg radius/full
typography/body/font-size typography/heading/line-height
```
**Primitives**: `blue/50` → `blue/900`, `gray/50` → `gray/900`
**Component names**: `Button`, `Input`, `Card`, `Avatar`, `Badge`, `Checkbox`, `Toggle`
**Variant names**: `Property=Value, Property=Value` — e.g., `Size=Medium, Style=Primary, State=Default`
**Page separators**: `---` (most common) or `——— COMPONENTS ———`
> Full naming reference: [naming-conventions.md](references/naming-conventions.md)
---
## 8. Token Architecture
| Complexity | Pattern |
|-----------|---------|
| < 50 tokens | Single collection, 2 modes (Light/Dark) |
| 50–200 tokens | **Standard**: Primitives (1 mode) + Color semantic (Light/Dark) + Spacing (1 mode) + Typography (1 mode) |
| 200+ tokens | **Advanced**: Multiple semantic collections, 4–8 modes (Light/Dark × Contrast × Brand). See M3 pattern in [token-creation.md](references/token-creation.md) |
Standard pattern (recommended starting point):
```
Collection: "Primitives" modes: ["Value"]
blue/500 = #3B82F6, gray/900 = #111827, ...
Collection: "Color" modes: ["Light", "Dark"]
color/bg/primary → Light: alias Primitives/white, Dark: alias Primitives/gray-900
color/text/primary → Light: alias Primitives/gray-900, Dark: alias Primitives/white
Collection: "Spacing" modes: ["Value"]
spacing/xs = 4, spacing/sm = 8, spacing/md = 16, ...
```
---
## 9. Per-Phase Anti-Patterns
**Phase 0 anti-patterns:**
- ❌ Ignoring existing file conventions and imposing new ones
- ❌ Skipping `search_design_system` before planning component creation
**Phase 1 anti-patterns:**
- ❌ Using `ALL_SCOPES` on 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 when `safeToRetryWithoutCanvasRead` is `false` before reading the canvas
- ❌ Using name-prefix matching for cleanup (deletes user-owned nodes)
- ❌ Building on unvalidated work from the previous step
- ❌ Parallelizing use_figma calls (always sequential)
- ❌ Guessing/hallucinating node IDs from memory (always read from state ledger)
- ❌ Writing massive inline scripts instead of using the provided helper scripts
- ❌ 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](references/discovery-phase.md) | 0 | **Required** | Starting any build — codebase analysis + Figma inspection |
| [token-creation.md](references/token-creation.md) | 1 | **Required** | Creating variables, collections, modes, styles |
| [documentation-creation.md](references/documentation-creation.md) | 2 | Required | Creating cover page, foundations docs, swatches |
| [component-creation.md](references/component-creation.md) | 3 | **Required** | Creating any component or variant |
| [code-connect-setup.md](references/code-connect-setup.md) | 3–4 | Required | Setting up Code Connect or variable code syntax |
| [naming-conventions.md](references/naming-conventions.md) | Any | Optional | Naming anything — variables, pages, variants, styles |
| [error-recovery.md](references/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](scripts/inspectFileStructure.js) | Discover all pages, components, variables, styles; returns full inventory |
| [createVariableCollection.js](scripts/createVariableCollection.js) | Create a named collection with modes; returns `{collectionId, modeIds}` |
| [createSemanticTokens.js](scripts/createSemanticTokens.js) | Create aliased semantic variables from a token map |
| [createComponentWithVariants.js](scripts/createComponentWithVariants.js) | Build a component set from a variant matrix; handles grid layout |
| [bindVariablesToComponent.js](scripts/bindVariablesToComponent.js) | Bind design tokens to all component visual properties |
| [createDocumentationPage.js](scripts/createDocumentationPage.js) | Create a page with title + description + section structure |
| [validateCreation.js](scripts/validateCreation.js) | Verify created nodes match expected counts, names, structure |
| [cleanupOrphans.js](scripts/cleanupOrphans.js) | Remove only the exact node, variable, and collection IDs supplied from the state ledger |
Scripts (8)
Read as text. anlyon.dev never runs them.
scripts/bindVariablesToComponent.jsShowHide
/**
* bindVariablesToComponent
*
* Binds design token variables to the visual properties of a component node.
* Supports fills, strokes, all padding directions, item spacing, and corner radius.
* Only binds properties for which a variable ID is provided in `bindings`.
*
* This function should be called on each variant individually within a component
* set, OR on the component set itself for properties shared by all variants.
*
* @param {ComponentNode | FrameNode | RectangleNode} component
* The Figma node to mutate. Usually a ComponentNode or one of its children.
* @param {{
* fills?: string,
* strokes?: string,
* paddingTop?: string,
* paddingBottom?: string,
* paddingLeft?: string,
* paddingRight?: string,
* itemSpacing?: string,
* cornerRadius?: string
* }} bindings
* Each key is a visual property name; each value is a Figma Variable ID
* (e.g. "VariableID:123:456"). Omit a key to skip binding that property.
* @returns {Promise<{ mutatedNodeIds: string[] }>}
* List of node IDs that were mutated (for audit/validation purposes).
*/
async function bindVariablesToComponent(component, bindings) {
const mutatedNodeIds = []
if (!component) {
return { mutatedNodeIds }
}
// Batch every getVariableByIdAsync call upfront in a single Promise.all rather
// than awaiting per-property — the lookups are independent and IPC-bound.
const floatBindings = [
['paddingTop', 'paddingTop'],
['paddingBottom', 'paddingBottom'],
['paddingLeft', 'paddingLeft'],
['paddingRight', 'paddingRight'],
['itemSpacing', 'itemSpacing'],
['cornerRadius', 'cornerRadius'],
]
const requestedIds = []
if (bindings.fills) requestedIds.push(['fills', bindings.fills])
if (bindings.strokes) requestedIds.push(['strokes', bindings.strokes])
for (const [bindingKey] of floatBindings) {
if (bindings[bindingKey]) requestedIds.push([bindingKey, bindings[bindingKey]])
}
const resolved = await Promise.all(
requestedIds.map(([, id]) => figma.variables.getVariableByIdAsync(id)),
)
const varByKey = {}
for (let i = 0; i < requestedIds.length; i++) {
varByKey[requestedIds[i][0]] = resolved[i]
}
const markMutated = () => {
if (!mutatedNodeIds.includes(component.id)) {
mutatedNodeIds.push(component.id)
}
}
// --- Fills ---
const fillVar = varByKey.fills
if (fillVar) {
const existingFills = component.fills
if (Array.isArray(existingFills) && existingFills.length > 0) {
// Bind the color of the first fill to the variable
const boundFill = figma.variables.setBoundVariableForPaint(existingFills[0], 'color', fillVar)
component.fills = [boundFill, ...existingFills.slice(1)]
} else {
// No existing fill — create a solid fill bound to the variable
const boundFill = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } },
'color',
fillVar,
)
component.fills = [boundFill]
}
markMutated()
}
// --- Strokes ---
const strokeVar = varByKey.strokes
if (strokeVar) {
const existingStrokes = component.strokes
if (Array.isArray(existingStrokes) && existingStrokes.length > 0) {
const boundStroke = figma.variables.setBoundVariableForPaint(
existingStrokes[0],
'color',
strokeVar,
)
component.strokes = [boundStroke, ...existingStrokes.slice(1)]
} else {
const boundStroke = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } },
'color',
strokeVar,
)
component.strokes = [boundStroke]
}
markMutated()
}
// --- Spacing properties (FLOAT variables bound via setBoundVariable) ---
for (const [bindingKey, figmaProp] of floatBindings) {
const variable = varByKey[bindingKey]
if (variable) {
component.setBoundVariable(figmaProp, variable)
markMutated()
}
}
return { mutatedNodeIds }
}
scripts/cleanupOrphans.jsShowHide
/**
* Removes entities identified by the exact IDs recorded in the state ledger.
*
* @param {{
* nodeIds?: string[],
* variableIds?: string[],
* collectionIds?: string[]
* }} ids
* @returns {Promise<{
* removedCount: number,
* removedIds: string[],
* skippedIds: string[]
* }>}
*/
async function cleanupOrphans({ nodeIds = [], variableIds = [], collectionIds = [] } = {}) {
const requestedIds = [...nodeIds, ...variableIds, ...collectionIds]
if (requestedIds.length === 0) {
throw new Error('cleanupOrphans: pass IDs from the state ledger.')
}
const removedIds = []
const skippedIds = []
const nodes = []
for (const id of new Set(nodeIds)) {
const node = await figma.getNodeByIdAsync(id)
if (!node) {
skippedIds.push(id)
continue
}
nodes.push(node)
}
for (const node of nodes.sort((a, b) => getDepth(b) - getDepth(a))) {
if (node.type === 'DOCUMENT') {
skippedIds.push(node.id)
continue
}
if (node.type === 'PAGE') {
if (figma.root.children.length <= 1) {
skippedIds.push(node.id)
continue
}
if (figma.currentPage.id === node.id) {
const fallbackPage = figma.root.children.find((page) => page.id !== node.id)
if (!fallbackPage) {
skippedIds.push(node.id)
continue
}
await figma.setCurrentPageAsync(fallbackPage)
}
}
removedIds.push(node.id)
node.remove()
}
for (const id of new Set(variableIds)) {
const variable = await figma.variables.getVariableByIdAsync(id)
if (!variable) {
skippedIds.push(id)
continue
}
removedIds.push(variable.id)
variable.remove()
}
for (const id of new Set(collectionIds)) {
const collection = await figma.variables.getVariableCollectionByIdAsync(id)
if (!collection) {
skippedIds.push(id)
continue
}
removedIds.push(collection.id)
collection.remove()
}
return {
removedCount: removedIds.length,
removedIds,
skippedIds,
}
}
function getDepth(node) {
let depth = 0
let current = node
while (current.parent) {
depth++
current = current.parent
}
return depth
}
scripts/createComponentWithVariants.jsShowHide
/**
* createComponentWithVariants
*
* Creates a component set by generating all combinations of `variantAxes`,
* building one Figma component per combination, then calling
* `figma.combineAsVariants` to produce the component set. After combining,
* the variants are repositioned into a grid so they don't all stack at (0, 0).
*
* @param {{
* name: string,
* description?: string,
* variantAxes: Record<string, string[]>,
* baseProps: {
* width: number,
* height: number,
* fills?: Paint[],
* padding?: {top?: number, bottom?: number, left?: number, right?: number},
* radius?: number,
* layoutMode?: 'HORIZONTAL' | 'VERTICAL' | 'NONE',
* itemSpacing?: number
* },
* page: PageNode
* }} config
* - `name`: Component set name (e.g. "Button").
* - `description`: Optional human-readable purpose and usage guidance.
* - `variantAxes`: Each key is a variant property name; each value is an array of
* allowed values. All combinations are generated (Cartesian product).
* Example: { Size: ['Small', 'Medium', 'Large'], Style: ['Primary', 'Ghost'] }
* produces 6 variants.
* - `baseProps`: Visual properties applied to every variant.
* - `page`: The PageNode to create components on (must be set as current page by caller).
* @returns {Promise<{
* componentSet: ComponentSetNode,
* variants: ComponentNode[]
* }>}
*/
async function createComponentWithVariants(config) {
const { name, variantAxes, baseProps, page } = config
// Ensure we are on the correct page
await figma.setCurrentPageAsync(page)
// Compute Cartesian product of variant axes
const axisNames = Object.keys(variantAxes)
const axisValues = axisNames.map((k) => variantAxes[k])
const combinations = cartesianProduct(axisValues)
// Build one component per combination
const components = []
for (const combo of combinations) {
const comp = figma.createComponent()
// Name: "Property=Value, Property=Value, ..."
comp.name = axisNames.map((ax, i) => `${ax}=${combo[i]}`).join(', ')
// Base geometry
comp.resize(baseProps.width, baseProps.height)
// Fills
if (baseProps.fills !== undefined) {
comp.fills = baseProps.fills
} else {
comp.fills = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }]
}
// Corner radius
if (baseProps.radius !== undefined) {
comp.cornerRadius = baseProps.radius
}
// Auto-layout
if (baseProps.layoutMode && baseProps.layoutMode !== 'NONE') {
comp.layoutMode = baseProps.layoutMode
comp.primaryAxisAlignItems = 'CENTER'
comp.counterAxisAlignItems = 'CENTER'
if (baseProps.itemSpacing !== undefined) {
comp.itemSpacing = baseProps.itemSpacing
}
}
// Padding
if (baseProps.padding) {
comp.paddingTop = baseProps.padding.top ?? 0
comp.paddingBottom = baseProps.padding.bottom ?? 0
comp.paddingLeft = baseProps.padding.left ?? 0
comp.paddingRight = baseProps.padding.right ?? 0
}
page.appendChild(comp)
components.push(comp)
}
// Combine into a component set
const componentSet = figma.combineAsVariants(components, page)
componentSet.name = name
if (config.description) {
componentSet.description = config.description
}
// Grid layout — variants stack at (0, 0) after combineAsVariants; reposition them.
const GRID_GAP = 16
const cols = Math.max(1, axisValues[axisValues.length - 1]?.length ?? 1)
const variantWidth = baseProps.width
const variantHeight = baseProps.height
componentSet.children.forEach((variant, idx) => {
const col = idx % cols
const row = Math.floor(idx / cols)
variant.x = col * (variantWidth + GRID_GAP)
variant.y = row * (variantHeight + GRID_GAP)
})
// Resize component set to wrap its children with padding
const totalCols = Math.min(cols, combinations.length)
const totalRows = Math.ceil(combinations.length / cols)
const PADDING = 40
componentSet.resize(
totalCols * variantWidth + (totalCols - 1) * GRID_GAP + PADDING * 2,
totalRows * variantHeight + (totalRows - 1) * GRID_GAP + PADDING * 2,
)
// Position component set at a safe canvas location
componentSet.x = 480
componentSet.y = 80
return { componentSet, variants: componentSet.children }
}
/**
* Computes the Cartesian product of multiple arrays.
* cartesianProduct([[A, B], [1, 2]]) → [[A,1], [A,2], [B,1], [B,2]]
*
* @param {Array<string[]>} arrays
* @returns {string[][]}
*/
function cartesianProduct(arrays) {
return arrays.reduce(
(acc, curr) => acc.flatMap((combo) => curr.map((val) => [...combo, val])),
[[]],
)
}
scripts/createDocumentationPage.jsShowHide
/**
* createDocumentationPage
*
* Creates a new Figma page with a standardized documentation layout: a page
* title, optional description, and an ordered list of sections each built by
* a caller-supplied `contentFn`. The content function receives the section
* frame and may append any nodes to it.
*
* This function is used for standalone documentation pages (e.g. a Foundations
* page, a Getting Started page, or a component page with documentation).
* It does not handle component sets — those live on separate pages created by
* createComponentWithVariants.
*
* @param {string} pageName - The Figma page name (e.g. "Foundations", "Getting Started").
* @param {{
* title: string,
* description?: string,
* sections: Array<{
* name: string,
* contentFn: (sectionFrame: FrameNode) => Promise<void>
* }>
* }} config
* - `title`: Large heading displayed at the top of the page.
* - `description`: Optional subtitle displayed below the heading.
* - `sections`: Ordered list of sections. Each section gets its own frame
* with a heading and is passed to `contentFn` for population.
* @returns {Promise<{
* page: PageNode,
* titleNode: TextNode,
* frameIds: string[]
* }>}
* `frameIds` is an ordered list of IDs for the root frame and each section frame.
*/
async function createDocumentationPage(pageName, config) {
// Verify required fonts are available before loading
const allFonts = await figma.listAvailableFontsAsync()
const requiredStyles = ['Bold', 'Regular', 'Medium']
for (const style of requiredStyles) {
const found = allFonts.some((f) => f.fontName.family === 'Inter' && f.fontName.style === style)
if (!found) {
const interFonts = allFonts.filter((f) => f.fontName.family === 'Inter')
throw new Error(
`Font "Inter ${style}" not available. Available Inter styles: ${interFonts.map((f) => f.fontName.style).join(', ') || 'none'}`,
)
}
}
await Promise.all([
figma.loadFontAsync({ family: 'Inter', style: 'Bold' }),
figma.loadFontAsync({ family: 'Inter', style: 'Regular' }),
figma.loadFontAsync({ family: 'Inter', style: 'Medium' }),
])
// Create and activate the page
const page = figma.createPage()
page.name = pageName
await figma.setCurrentPageAsync(page)
const frameIds = []
// Root scroll container — 1440px wide, auto-height
const root = figma.createAutoLayout('VERTICAL')
root.name = pageName
root.primaryAxisAlignItems = 'MIN'
root.counterAxisAlignItems = 'MIN'
root.itemSpacing = 80
root.paddingTop = 80
root.paddingBottom = 120
root.paddingLeft = 80
root.paddingRight = 80
root.resize(1440, 1)
root.layoutSizingHorizontal = 'FIXED'
root.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]
root.x = 0
root.y = 0
page.appendChild(root)
frameIds.push(root.id)
// Page header: title + optional description
const header = figma.createAutoLayout('VERTICAL')
header.name = 'Header'
header.itemSpacing = 12
header.fills = []
root.appendChild(header)
header.layoutSizingHorizontal = 'FILL'
const titleNode = figma.createText()
titleNode.fontName = { family: 'Inter', style: 'Bold' }
titleNode.characters = config.title
titleNode.fontSize = 40
titleNode.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }]
header.appendChild(titleNode)
titleNode.layoutSizingHorizontal = 'FILL'
if (config.description) {
const descNode = figma.createText()
descNode.fontName = { family: 'Inter', style: 'Regular' }
descNode.characters = config.description
descNode.fontSize = 16
descNode.lineHeight = { value: 24, unit: 'PIXELS' }
descNode.fills = [{ type: 'SOLID', color: { r: 0.4, g: 0.4, b: 0.4 } }]
header.appendChild(descNode)
descNode.layoutSizingHorizontal = 'FILL'
}
// Sections
for (const section of config.sections) {
const sectionFrame = figma.createAutoLayout('VERTICAL')
sectionFrame.name = `Section/${section.name}`
sectionFrame.itemSpacing = 20
sectionFrame.fills = []
root.appendChild(sectionFrame)
sectionFrame.layoutSizingHorizontal = 'FILL'
// Section heading
const sectionHeading = figma.createText()
sectionHeading.fontName = { family: 'Inter', style: 'Bold' }
sectionHeading.characters = section.name
sectionHeading.fontSize = 24
sectionHeading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }]
sectionFrame.appendChild(sectionHeading)
sectionHeading.layoutSizingHorizontal = 'FILL'
// Invoke the caller's content function to populate the section
await section.contentFn(sectionFrame)
frameIds.push(sectionFrame.id)
}
return { page, titleNode, frameIds }
}
scripts/createSemanticTokens.jsShowHide
/**
* createSemanticTokens
*
* Creates a batch of Figma variables in the given collection, one per entry in
* `tokenMap`. Supports raw values, variable alias references, code syntax, and
* scopes. Returns a map of token name → Variable for use in subsequent steps.
*
* @param {VariableCollection} collection - The target variable collection.
* @param {Record<string, string>} modeIds - Map of {modeName: modeId} from createVariableCollection.
* @param {Array<{
* name: string,
* type: 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN' | 'TIMING' | 'EASING',
* values: Record<string, string | number | boolean | MotionEasing | {type: 'VARIABLE_ALIAS', id: string}>,
* scopes?: VariableScope[],
* codeSyntax?: {WEB?: string, ANDROID?: string, iOS?: string}
* }>} tokenMap - Ordered list of token definitions.
* - `name`: Variable name using slash hierarchy (e.g. "color/bg/primary").
* - `type`: Figma variable type.
* - `values`: Map of {modeName: value}. Values can be raw (hex string for COLOR,
* number for FLOAT) or alias objects {type: 'VARIABLE_ALIAS', id: variableId}.
* For COLOR, raw values are accepted as hex strings ("#rrggbb" or "#rrggbbaa")
* and converted to {r, g, b, a} automatically.
* - `scopes`: Array of VariableScope strings. Omit to use [] (hidden/primitive).
* - `codeSyntax`: Platform code syntax strings. Omit to skip.
* @returns {Promise<{variables: Record<string, Variable>}>}
* `variables` maps each token name to its created Variable object.
*/
async function createSemanticTokens(collection, modeIds, tokenMap) {
const variables = {}
for (const token of tokenMap) {
// Create the variable
const variable = figma.variables.createVariable(token.name, collection, token.type)
// Set values for each mode
for (const [modeName, rawValue] of Object.entries(token.values)) {
const modeId = modeIds[modeName]
if (!modeId) {
throw new Error(
`createSemanticTokens: mode "${modeName}" not found in modeIds for token "${token.name}". ` +
`Available modes: ${Object.keys(modeIds).join(', ')}`,
)
}
let value = rawValue
// Convert hex strings to Figma RGBA for COLOR type
if (token.type === 'COLOR' && typeof rawValue === 'string' && rawValue.startsWith('#')) {
value = hexToFigmaColor(rawValue)
}
variable.setValueForMode(modeId, value)
}
// Set scopes (default: empty array = hidden from property pickers / primitives)
variable.scopes = token.scopes || []
// Set code syntax per platform
if (token.codeSyntax) {
if (token.codeSyntax.WEB) {
variable.setVariableCodeSyntax('WEB', token.codeSyntax.WEB)
}
if (token.codeSyntax.ANDROID) {
variable.setVariableCodeSyntax('ANDROID', token.codeSyntax.ANDROID)
}
if (token.codeSyntax.iOS) {
variable.setVariableCodeSyntax('iOS', token.codeSyntax.iOS)
}
}
variables[token.name] = variable
}
return { variables }
}
/**
* Converts a hex color string to a Figma RGBA object.
* Supports "#rgb", "#rrggbb", and "#rrggbbaa".
*
* @param {string} hex
* @returns {{ r: number, g: number, b: number, a: number }}
*/
function hexToFigmaColor(hex) {
let h = hex.replace('#', '')
// Expand shorthand #rgb → #rrggbb
if (h.length === 3) {
h = h
.split('')
.map((c) => c + c)
.join('')
}
const r = parseInt(h.substring(0, 2), 16) / 255
const g = parseInt(h.substring(2, 4), 16) / 255
const b = parseInt(h.substring(4, 6), 16) / 255
const a = h.length === 8 ? parseInt(h.substring(6, 8), 16) / 255 : 1
return { r, g, b, a }
}
scripts/createVariableCollection.jsShowHide
/**
* createVariableCollection
*
* Creates a new Figma variable collection with the specified name and modes.
* If `modeNames` has more than one entry, the first mode is renamed from
* Figma's default "Mode 1" to the first name, and additional modes are added.
*
* @param {string} name - The display name of the collection (e.g. "Color", "Spacing").
* @param {string[]} modeNames - Ordered list of mode names (e.g. ["Light", "Dark"] or ["Value"]).
* @returns {Promise<{
* collection: VariableCollection,
* modeIds: Record<string, string>
* }>}
* `modeIds` maps each mode name to its modeId string.
*/
async function createVariableCollection(name, modeNames) {
if (!modeNames || modeNames.length === 0) {
throw new Error('createVariableCollection: modeNames must have at least one entry.')
}
// Create the collection — Figma always creates it with one mode named "Mode 1".
const collection = figma.variables.createVariableCollection(name)
// modeIds accumulator
const modeIds = {}
// Rename the default first mode
const defaultMode = collection.modes[0]
collection.renameMode(defaultMode.modeId, modeNames[0])
modeIds[modeNames[0]] = defaultMode.modeId
// Add additional modes
for (let i = 1; i < modeNames.length; i++) {
const newModeId = collection.addMode(modeNames[i])
modeIds[modeNames[i]] = newModeId
}
return { collection, modeIds }
}
scripts/inspectFileStructure.jsShowHide
/**
* inspectFileStructure
*
* Reads the current Figma file and returns a structural inventory:
* all pages (with child counts), all local variable collections (with mode
* names and variable counts), all component sets, all local text styles,
* and all local effect styles.
*
* This is a read-only discovery function — it never creates or mutates nodes.
* Run it at the start of Phase 0 to understand what already exists before
* planning any creation work.
*
* @returns {Promise<{
* pages: Array<{id: string, name: string, childCount: number}>,
* variableCollections: Array<{
* id: string,
* name: string,
* modes: Array<{modeId: string, name: string}>,
* variableCount: number,
* variableNames: string[]
* }>,
* componentSets: Array<{id: string, name: string, variantCount: number, pageId: string, pageName: string}>,
* textStyles: Array<{id: string, name: string, fontFamily: string, fontStyle: string, fontSize: number}>,
* effectStyles: Array<{id: string, name: string, effectCount: number}>
* }>}
*/
async function inspectFileStructure() {
const result = {
pages: [],
variableCollections: [],
componentSets: [],
textStyles: [],
effectStyles: [],
}
// --- Pages ---
for (const page of figma.root.children) {
result.pages.push({
id: page.id,
name: page.name,
childCount: page.children.length,
})
}
// --- Variable collections ---
const collections = await figma.variables.getLocalVariableCollectionsAsync()
for (const coll of collections) {
const variables = await Promise.all(
coll.variableIds.map((id) => figma.variables.getVariableByIdAsync(id)),
)
const variableNames = variables.filter(Boolean).map((v) => v.name)
result.variableCollections.push({
id: coll.id,
name: coll.name,
modes: coll.modes.map((m) => ({ modeId: m.modeId, name: m.name })),
variableCount: coll.variableIds.length,
variableNames,
})
}
// --- Component sets (and standalone components) ---
// We need to load all pages to inspect components across the whole file.
const originalPage = figma.currentPage
for (const page of figma.root.children) {
await figma.setCurrentPageAsync(page)
// findAllWithCriteria.types accepts an array — one indexed scan returns
// both COMPONENT_SET and standalone COMPONENT nodes.
const found = page.findAllWithCriteria({ types: ['COMPONENT_SET', 'COMPONENT'] })
for (const node of found) {
if (node.type === 'COMPONENT_SET') {
result.componentSets.push({
id: node.id,
name: node.name,
variantCount: node.children.length,
pageId: page.id,
pageName: page.name,
})
} else if (node.parent && node.parent.type !== 'COMPONENT_SET') {
// Standalone component (not a variant inside a COMPONENT_SET)
result.componentSets.push({
id: node.id,
name: node.name,
variantCount: 1,
pageId: page.id,
pageName: page.name,
})
}
}
}
// Restore original page
await figma.setCurrentPageAsync(originalPage)
// --- Text styles ---
const textStyles = figma.getLocalTextStyles()
for (const ts of textStyles) {
result.textStyles.push({
id: ts.id,
name: ts.name,
fontFamily: ts.fontName.family,
fontStyle: ts.fontName.style,
fontSize: ts.fontSize,
})
}
// --- Effect styles ---
const effectStyles = figma.getLocalEffectStyles()
for (const es of effectStyles) {
result.effectStyles.push({
id: es.id,
name: es.name,
effectCount: es.effects.length,
})
}
return result
}
scripts/validateCreation.jsShowHide
/**
* validateCreation
*
* Verifies that a set of nodes exist and match expected structural properties.
* Designed to run immediately after a creation script to catch partial failures
* before proceeding to the next build phase.
*
* Each check specifies a node ID and any combination of expected properties.
* A check passes when all specified expectations are met; it fails (with a
* reason string) as soon as any expectation is violated.
*
* @param {Array<{
* nodeId: string,
* expectedChildCount?: number,
* expectedName?: string,
* expectedType?: NodeType
* }>} checks
* - `nodeId`: The Figma node ID to look up via figma.getNodeById.
* - `expectedChildCount`: If set, the node must have exactly this many direct children.
* Applies to any node with a `children` property (frames, component sets, etc.).
* - `expectedName`: If set, the node's `.name` must exactly match this string.
* - `expectedType`: If set, the node's `.type` must exactly match this string.
* @returns {{
* passed: string[],
* failed: Array<{nodeId: string, reason: string}>
* }}
* `passed`: Array of nodeIds that passed all checks.
* `failed`: Array of objects with the nodeId and a human-readable reason string.
*/
function validateCreation(checks) {
const passed = []
const failed = []
for (const check of checks) {
const node = figma.getNodeById(check.nodeId)
// Node must exist
if (!node) {
failed.push({
nodeId: check.nodeId,
reason: `Node not found. It may not have been created, or was deleted.`,
})
continue
}
const reasons = []
// Type check
if (check.expectedType !== undefined && node.type !== check.expectedType) {
reasons.push(`type is "${node.type}", expected "${check.expectedType}"`)
}
// Name check
if (check.expectedName !== undefined && node.name !== check.expectedName) {
reasons.push(`name is "${node.name}", expected "${check.expectedName}"`)
}
// Child count check
if (check.expectedChildCount !== undefined) {
if (!('children' in node)) {
reasons.push(
`node type "${node.type}" does not have children, but expectedChildCount=${check.expectedChildCount} was specified`,
)
} else {
const actualCount = node.children.length
if (actualCount !== check.expectedChildCount) {
reasons.push(`has ${actualCount} children, expected ${check.expectedChildCount}`)
}
}
}
if (reasons.length > 0) {
failed.push({
nodeId: check.nodeId,
reason: reasons.join('; '),
})
} else {
passed.push(check.nodeId)
}
}
return { passed, failed }
}
