figma-create-ui — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited figma-create-ui (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Use FigCraft declarative tools for UI creation — NOT use_figma. create_frame includes an Opinion Engine that auto-handles sizing inference, FILL ordering, token binding, and failure cleanup. This eliminates common Figma API pitfalls that use_figma scripts must handle manually.
This SKILL.md is a lightweight entry point. The detailed, context-aware creation guidance is delivered at runtime by get_mode._workflow. Call get_mode first — it returns a _workflow object containing:
designPreflight — blocking checklist (purpose, platform, language, density, tone, color/typography rules)creationSteps — ordered steps with tool-specific guidancetoolBehavior — key rules for batch tools and ordered executionreferences — links to on-demand guides via MCP toolssearchBehavior — whether search_design_system is available in current modenextAction — what to do immediatelyFollow `_workflow` as your primary guide. This SKILL.md provides the boundaries, decision rules, and best practices that _workflow does not cover.
_workflow (designPreflight + creationSteps + references). If `get_mode` fails (plugin not connected) → STOP. Do not fall back to other MCP servers. Tell user: open Figma → Plugins → FigCraft → wait for connection, then retry.dryRun:true to preview)create_frame, create_text).search_design_system and type:"instance" as described in the Library Components section below.When get_mode returns libraryComponents, always prefer component instances over hand-built frames.
⛔ MANDATORY: Always search before building. The design system likely has the component you need.
search_design_system: search_design_system(query: "button")
search_design_system(query: "input")containingFrame: "Forms" → form components (inputs, selects, checkboxes)containingFrame: "Avatars" → profile/user image components — NEVER use for form inputs components(method: "list_properties", nodeId: "<component-id>") Component Map:
- Button → key: "abc123", variants: Type=Primary/Secondary, Size=Small/Medium/Large
- Input → key: "def456", variants: Size=md/lg, State=Default/Focused/Error
- Card → key: "ghi789", standalone componentUse create_frame with type:"instance" in children:
| Build manually | Import from design system |
|---|---|
| Page wrapper frame | Components: buttons, cards, inputs, toggles, tabs, etc. |
| Section container frames | Variables: colors (fillVariableId PREFERRED / fillVariableName), spacing, radii |
| Layout structure (rows, columns) | Text styles: textStyleName: "body-md" |
| Dividers, spacers | Effect styles: effectStyleName: "elevation-200" |
{ "type": "instance", "componentSetKey": "abc123", "variantProperties": { "Type": "Primary", "Size": "Large" } } { "type": "instance", "componentKey": "abc123", "properties": { "Label": "Sign In" } }variantProperties)Before creating: call get_design_context to grab variable IDs from defaults.*.id — each subsequent call that uses the ID path skips name resolution entirely.
// Assume get_design_context returned:
// defaults.surfacePrimary.id = "VariableID:123:45"
// defaults.textPrimary.id = "VariableID:123:67"
// defaults.textSecondary.id = "VariableID:123:68"
{
"name": "Screen / Login",
"width": 402, "height": 874,
"layoutMode": "VERTICAL",
"primaryAxisAlignItems": "SPACE_BETWEEN",
"role": "screen",
"fillVariableId": "VariableID:123:45",
"children": [
{
"type": "frame", "name": "Top Content",
"layoutMode": "VERTICAL", "itemSpacing": 32,
"padding": 24, "paddingTop": 80,
"children": [
{ "type": "text", "content": "Welcome back", "textStyleName": "display-md", "fontColorVariableId": "VariableID:123:67" },
{ "type": "text", "content": "Sign in to your account", "textStyleName": "body-md", "fontColorVariableId": "VariableID:123:68" },
{
"type": "instance", "componentKey": "INPUT_KEY",
"properties": { "Placeholder": "[email protected]", "Size": "lg" }
},
{
"type": "instance", "componentKey": "INPUT_KEY",
"properties": { "Placeholder": "Password", "Size": "lg" }
}
]
},
{
"type": "frame", "name": "Bottom Content",
"layoutMode": "VERTICAL", "itemSpacing": 16,
"padding": 24, "paddingBottom": 48,
"children": [
{
"type": "instance", "componentKey": "BUTTON_KEY",
"properties": { "Label": "Sign In", "Type": "Primary", "Size": "Large" },
"layoutSizingHorizontal": "FILL"
}
]
}
]
}When the task is creating new reusable components (buttons, inputs, cards with variants) — not assembling screens from existing library components — use create_component instead of create_frame.
create_component over create_framecreate_component delegates to `create_frame` internally — you get all Opinion Engine inferences (sizing, FILL ordering, token auto-binding) for freecomponentPropertyNamecreate_frame → create_component_from_node two-step conversionAll 5 tools below are core — no load_toolset needed.
1. create_section(name:"Button") → organize components (auto-positions below existing content)
2. create_component({items:[...]}) → batch-create variants (library mode: prefer fillVariableId/fontColorVariableId from get_design_context; fall back to *Name only when ID unavailable)
⛔ EXPLICIT PARAMS: itemSpacing, padding, cornerRadius MUST be set in every item — do NOT rely on Opinion Engine inference across batches
⛔ ICON COLOR: colorVariableName on stroke-based icons (lucide etc.) will be silently skipped due to scope mismatch — plan for post-creation binding
⛔ BOOLEAN: children with visibility toggles MUST declare componentPropertyReferences:{visible:"PropName"} inline — properties:[] alone is orphaned
⛔ FULL-WIDTH: use explicit width (e.g. 360) for full-width button variants — FILL is not supported inside ComponentSet
3. create_component_set → combine + auto-layout + auto-position
4. bind_component_property({nodeId:"<setId>", variantFilter:{Type:"Emphasis"}, bindings:[...]}) → post-creation icon color binding per Type group using nodeProperty:"iconColor"
5. export_image → verify a representative variant's icon color and visibility BEFORE proceedingFor component property management (add_component_property, update_component_property), load_toolset("components-advanced"). Note: bind_component_property is a core tool — no toolset loading needed.
⛔ When the target component has 8 or more variants, you MUST output a markdown difference matrix BEFORE calling any `create_component` / `nodes(method:"clone")` / `bind_component_property`. Skipping this step is the single largest failure mode for multi-variant component creation — it produces clone-then-patch chains where each fix exposes another inconsistency, and the agent burns 10+ rounds chasing variant drift.
⛔ Before filling the matrix, define each unique structure as a children tree. A structure label like "std" is NOT sufficient — you must specify the full nesting with layoutMode, sizing, and alignment for every level. Example:
Structure "std" (button with optional icons):
Button (HORIZONTAL, CENTER, FILL, padding 16)
└── Content (HORIZONTAL, CENTER, FILL, itemSpacing 4)
├── Icon-left (16px, BOOLEAN hidden)
├── Label (HUG)
└── Icon-right (16px, BOOLEAN hidden)
Structure "spinner-only" (loading state):
Button (HORIZONTAL, CENTER, FILL)
└── Spinner (20px SVG)The structure column in the matrix references these defined trees by name:
| Variant | base fill | textColor | iconColor | padding | structure | notes |
|---|---|---|---|---|---|---|
| Emphasis / Default / sm | button/emphasis | text/inverse | text/inverse | 12/6 | std | — |
| Emphasis / Default / md | button/emphasis | text/inverse | text/inverse | 16/8 | std | — |
| Tertiary / Default / md | transparent | text/primary | text/primary | 16/8 | std | no fill |
| * / Loading / md | inherit | inherit | — | inherit | spinner-only | replaces children |
| * / Disabled / md | surface/disabled | text/disabled | text/disabled | inherit | std | — |
Without this matrix you will:
iconColor rebound (icon Vectors stay on Emphasis color after clone)After the matrix is filled in, build in this order: (1) batch-create all std variants per Type×Size×State using create_component({items:[...]}), (2) batch-create all Loading variants separately (different children structure), (3) create_component_set to combine all, (4) bind_component_property with variantFilter to bind iconColor per Type slice.
Loading is a STRUCTURAL variant, not a visual variant. Loading replaces children with a spinner — it's not just a different color/state of the standard structure. Build Loading variants separately:
1. Build base "Default" with [icon-left, label, icon-right]
2. Clone Default → Hover / Active / Pressed / Focused (5 visual states share structure)
3. Build SEPARATE base "Loading" with [spinner] children
4. Clone Loading → Loading × Type matrix
5. Combine all into one ComponentSetIf you try to clone Default → Loading and then "hide icon-left, hide label, show spinner", you will end up with 8 broken Loading variants whose children don't match the rest of the design system's spinner spec.
variantFilter)bind_component_property accepts variantFilter: { Type: "Tertiary" } (or any property combo) to limit a binding to a subset of variants. Use this whenever Type/State/Size determines a different value:
{
"nodeId": "<componentSetId>",
"variantFilter": { "Type": "Tertiary" },
"bindings": [
{ "targetNodeSelector": "Icon-left", "nodeProperty": "iconColor", "value": "icon/primary" },
{ "targetNodeSelector": "Icon-right", "nodeProperty": "iconColor", "value": "icon/primary" }
]
}nodeProperty: "iconColor" is a build-time bulk-apply — it walks the matched node's Vector descendants and rebinds their fills/strokes. Use it INSTEAD of cloning + manually editing each variant's icon. Value auto-detection: #hex → hex, VariableID:... → direct ID binding, bare name → variable name lookup.
fillVariableId / fontColorVariableId from get_design_context.defaults.*.id. Fall back to *Name only when the ID is unavailable. figcraft returns a "next time use fontColorVariableId" typed hint after a successful name lookup — use it on subsequent calls. Text binding failures write a magenta sentinel fill so black-on-black bugs are visible in screenshots.colorVariableName on icon children is silently skipped when the variable's scopes exclude STROKE_COLOR (most text/* variables). After create_component_set, you MUST call bind_component_property with nodeProperty:"iconColor" per Type group using variantFilter. This is the ONLY reliable path for stroke-based icons (lucide, tabler, etc.). Skipping this step leaves all icons hardcoded black — visually broken on dark backgrounds.properties:[{type:"BOOLEAN", propertyName:"Icon", defaultValue:false}] AND componentPropertyReferences:{visible:"Icon"} on the child whose visibility you want to control. Both sides are required — property alone creates an orphaned toggle that does nothing; reference alone is a no-op. The child must have a name field. figcraft auto-wires + syncs node visibility with defaultValue. See create_component schema for the full example.create_component({items:[...]}), explicitly set itemSpacing, padding*, and cornerRadius in every item. Opinion Engine inference can produce different values across batches (e.g. itemSpacing: 8 vs 4), causing inconsistency within the same ComponentSet.layoutSizingHorizontal:"FILL" (Figma platform limitation — ComponentSet uses absolute positioning). Use explicit width (e.g. 360) to represent full-width intent. Consumers apply FILL when placing instances.Property=Value, Property=Value format (e.g., "Size=Small, Style=Primary, State=Default").create_component_set enforces a soft 30-variant cap. Pass variantLimit:0 to disable for legitimate large matrices. Consider extracting high-cardinality axes (icons, colors) into INSTANCE_SWAP properties instead.create_component({items:[...]}) is capped at 10 items per call (lowered from 20 after timeout incidents). For 32-variant components, split into ⌈N/10⌉ sequential calls.strokeColor / fill (singular, hex string) or the *VariableId / *VariableName aliases. Passing the raw plural Plugin API names will throw a self-correcting error pointing at the right alias.For components with 3+ variants or complex variant matrices, reference these patterns:
.Building Blocks/ component sets to prevent variant explosion (see component-creation reference Section 1)variables_ep(method:"batch_bind")textStyleName for typography, effectStyleName for shadowsaudit_node(nodeId) + export_image after each component set__ prefix for internal helpers, . prefix for hidden from assets panelFor full design system builds (10+ components + variables + theming), switch to figcraft-generate-library.
create_frame includes an Opinion Engine that automatically infers best practices. Key behaviors:
fillVariableId/strokeVariableId (PREFERRED, from get_design_context) or fillVariableName/strokeVariableName (fallback) matched to library variablesUse dryRun:true to preview all inferences before creating nodes. For full details: get_creation_guide(topic:"opinion-engine").
layoutSizingHorizontal:"FIXED" + layoutSizingVertical:"FIXED". Without this, Opinion Engine infers HUG and the frame collapses.role: "screen". Buttons SHOULD include role: "button". This enables deterministic lint identification instead of name-regex guessing, especially important for non-English names.layoutSizingVertical: "FILL" explicitly — Opinion Engine defaults to HUG which defeats SPACE_BETWEEN.type:"frame" (not "rectangle"), because rectangles cannot have children.icon_search + icon_create with index param for ordering. NEVER text characters as placeholders. See iconography.icon_create appends to END by default — use index: 0 to place BEFORE text. Array order = visual order in auto-layout. Full patterns: get_creation_guide(topic:"iconography").export_image to catch issues early.When updating rather than creating:
nodes(method: "get", nodeId: "<screen-id>") — inspect current structurenodes(method: "update", patches: [...]) — update properties, swap variantsnodes(method: "delete", nodeId: "<old-section>") — remove deprecated sectionscreate_frame(parentId: "<screen-id>", ...)export_image after each modificationCall these MCP tools when you need deeper guidance on a specific topic:
| Guide | When to load |
|---|---|
get_creation_guide(topic:"layout") | Structural layout rules (39 rules from Quality Engine) |
get_creation_guide(topic:"multi-screen") | Multi-screen flow details (skill multi-screen-flow is auto-loaded at STEP 4) |
get_creation_guide(topic:"batching") | Context budget and batching strategy |
get_creation_guide(topic:"opinion-engine") | Full Opinion Engine auto-inference documentation |
get_creation_guide(topic:"ui-patterns", uiType:"xxx") | UI type templates (login, dashboard, settings, etc.) |
get_creation_guide(topic:"responsive") | Responsive web breakpoints + auto-layout |
get_creation_guide(topic:"content-states") | Empty/loading/error state patterns |
get_design_guidelines(category) | Design direction rules (color, typography, spacing, etc.) |
Design rules are delivered by _workflow.designPreflight (from get_mode). For detailed rules by category, call get_design_guidelines(category).
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.