Composition
Complete Composition reference for SXL Studio: root fields, structure, tags, props, component properties, styles, transitions, and theme binding — every section opens with a field table.
Overview
$type: "composition" is the SXL Studio contract that describes a Figma component as JSON. It is the single source of truth that turns design intent into repeatable Generate and Apply runs in Figma.
At a glance, a composition is built from these blocks:
| Block | Key(s) | Purpose |
|---|---|---|
| Structure | structure | The layer tree |
| Variants | props / states | Variant axes and mutually-exclusive states |
| Styling | styles | Layout, appearance, and per-variant overrides |
| Properties | componentProperties | Native Figma properties on instances |
| Behavior | transitions / themeBinding | Prototyping and light/dark theming |
Key idea One JSON file describes the whole component set — its shape, its variants, and its styling — so the same result can be generated, updated, and handed off to engineers or agents without manual rebuilding.
Bitmap images and mirrored layers
Available from version 2.9.4.
An image paint can use src: "data:image/png;base64,..." (replace the placeholder with valid bytes), an HTTP(S) source, or imageHash for an image already available in the same Figma file. Explicit source fields take priority in this order: src, url, href, then imageHash. An invalid explicit source produces an error; it does not fall back to a hash. A hash alone is not portable between files.
Embedded sources support PNG, JPEG and GIF, with at most 4 MiB of decoded bytes per image. Full JSON export limits the combined embedded data-URL text to 16 MiB of characters, counting repeated occurrences. Figma documents a maximum image dimension of 4096 pixels per axis. These limits do not imply that every remote URL can be fetched successfully.
Get Code embeds original bitmap bytes in full JSON by default. If bytes are unavailable or exceed the supported limits, export fails explicitly. Original bytes do not include the layer's crop, filters or effects: these remain separate properties. Image paints preserve imageTransform for CROP, filters, visibility and blend mode. Regular nested rectangles remain RECTANGLE layers.
styles.relativeTransform accepts a finite native 2×3 matrix with independent unit-length axes, including reflection. Use width and height for size; this is not a CSS scale transform. Root page position is retained, and auto-layout controls the position of flow children. For absolutely positioned children and free layout, the matrix also carries the child's position. This node transform is separate from the image paint's crop transform.
Mixed effect lists preserve their order, including GLASS effects without native variable bindings. Raw mode is an explicit literal snapshot; it does not preserve variable links. Figma marks GLASS as beta, so runtime support and visual acceptance still need verification.
Instance scale
In the upcoming 2.9.5 version, scale sets the absolute scale of an explicitly declared INSTANCE or ICON layer, including an INSTANCE inside a native SLOT in a main component.
{
"structure": {
"tag": "FRAME",
"class": "root",
"children": [
{
"tag": "INSTANCE",
"class": "preview",
"ref": { "component": "PreviewCard" }
}
]
},
"styles": {
".root": { "direction": "column" },
".preview": { "width": 200, "height": 80, "scale": 1.03 }
}
}
width and height are specified before scaling: this example produces 206 × 82.4. Repeating Apply with 1.03 does not accumulate scale. Changing scale sets a new absolute factor; removing the last authored scale returns the instance to 1. Manual scale is preserved on a layer whose scale has never been authored in JSON.
Use a number from 0.01 to 100. Strings and token references are unsupported. The anchor is the top-left corner; auto-layout uses the resulting size and controls the layer's position. Combining scale with FILL sizing is rejected.
Numeric min/max limits scale with the instance while retaining variable references: maxWidth 210 with scale: 1.03 produces a 216.3 limit. The source variable value is unchanged.
With proportions locked, Figma couples the axes: writing a dimension or min/max variable on one axis can remove bindings on the other. The precise rule: with the ratio locked, each pair width/height, minWidth/minHeight, maxWidth/maxHeight holds one variable; different pairs do not interfere (minWidth together with maxHeight is fine), and the same token on both sides of a pair is allowed: Figma keeps one binding, the size follows the lock and the result equals the authored intent (the icon idiom width: {size}, height: {size}). The authoring error is different variables on the two sides of one pair under a JSON lock: Generate and Apply stop before any change with the error status (aspect-both-axes), name the layer, the pair and the tokens, and propose the JSON fix — keep one variable per pair (the side the parent drives; the other follows the ratio) or remove the lock from that layer. A lock that lives on the canvas rather than in JSON (inherited from the main component or set by hand) is not an error: the authored side is written, the other side's binding is released and the result carries an aspect-lock-settled warning. A failing root style of a variant is reported in the result's errors instead of leaving the variant empty. An inherited lock on an instance (for example an icon whose main component is locked) is not a conflict: the JSON does not own that lock, the instance receives its variables the usual way, Figma keeps one variable per pair and the size follows the lock. With the same token on both axes the result is fully equivalent to the authored intent and Smart Apply reports no discrepancy; with different tokens Smart keeps the live side and warns. A lock left on a managed layer without a key in JSON is removed by a full Apply before the variables are written. This is Figma behaviour, not the plugin: with the ratio locked each pair width/height, minWidth/minHeight, maxWidth/maxHeight keeps only one variable, the other is removed and unlocking does not bring it back. For cards in a grid choose the controlling axis: widthType: "fill" or a width variable, minWidth/maxWidth and aspectRatio — height and its limits follow the ratio, so height variables are unnecessary. Use limits on the controlling axis together with the ratio, or unlock proportions for independent limits on both axes. Supported JSON settings are aspectRatio / targetAspectRatio and constrainProportions; lockAspectRatio is a Figma method name, not an accepted JSON setting.
Do not put scale on the composition root, FRAME / SLOT containers, in ref.styles, or on an instance inside another instance. For slot default content, declare an INSTANCE child and set scale on its class in styles. Authored layer types, placement of scale and combinations with FILL in styles are checked before generation. Limits that depend on the current Figma instance are checked during application. Arbitrary container scaling is unsupported because the native Figma operation can remove numeric bindings from descendant layers.
Card hover without layout shifts
To enlarge the whole card — image, badges and button — inside Auto Layout or Grid, keep its cell in the layout flow and position the scaled instance absolutely inside it. The cell keeps the same size in both states, so neighboring cards do not move. No separate scaleMode property is needed.
Prepare a CardArtwork component containing the whole card. This example uses a 172:230 ratio. Create number variables card.width, card.height, card.minWidth, card.maxWidth, card.minHeight, card.maxHeight; for example, with values 172, 230, 86, 258, 115 and 345.
First generate CardEnvelope. Its root holds the size and all four limits with aspect locking disabled. The inner CardArtwork instance has an explicit aspect ratio:
{
"$type": "composition",
"name": "CardEnvelope",
"structure": {
"tag": "FRAME",
"class": "envelope",
"children": [
{
"tag": "INSTANCE",
"class": "artwork",
"ref": {
"component": "CardArtwork"
}
}
]
},
"styles": {
".envelope": {
"direction": "row",
"widthType": "fixed",
"heightType": "fixed",
"width": "{card.width}",
"height": "{card.height}",
"minWidth": "{card.minWidth}",
"maxWidth": "{card.maxWidth}",
"minHeight": "{card.minHeight}",
"maxHeight": "{card.maxHeight}",
"justifyContent": "center",
"alignItems": "center",
"clipsContent": false,
"constrainProportions": false
},
".artwork": {
"widthType": "fill",
"heightType": "fixed",
"constrainProportions": true,
"aspectRatio": "172/230"
}
}
}
Then generate CardCell. Its footprint matches the envelope's base size, while the absolute envelope instance grows on hover:
{
"$type": "composition",
"name": "CardCell",
"props": {
"state": ["default", "hover"]
},
"structure": {
"tag": "FRAME",
"class": "cell",
"children": [
{
"tag": "INSTANCE",
"class": "card",
"ref": {
"component": "CardEnvelope"
}
}
]
},
"styles": {
".cell": {
"direction": "row",
"widthType": "fixed",
"heightType": "fixed",
"width": "{card.width}",
"height": "{card.height}",
"minWidth": "{card.minWidth}",
"maxWidth": "{card.maxWidth}",
"minHeight": "{card.minHeight}",
"maxHeight": "{card.maxHeight}",
"clipsContent": false,
"constrainProportions": false
},
".card": {
"position": "absolute",
"widthType": "fixed",
"heightType": "fixed",
"width": "{card.width}",
"height": "{card.height}",
"scale": 1,
"x": 0,
"y": 0,
"constraintH": "scale",
"constraintV": "scale",
"constrainProportions": false
},
"$state=hover .card": {
"scale": 1.03,
"x": "-1.5%",
"y": "-1.5%"
}
},
"transitions": {
"$state=default -> $state=hover": "on-hover smart-animate 160ms ease-out"
}
}
Place several CardCell instances with state: "default" as normal children of an Auto Layout row or Grid. Each card owns its transition; no reaction on the whole row is needed. At scale: 1.03, a 172 × 230 card becomes 177.16 × 236.9, while its cell stays 172 × 230.
The -1.5% offset on each axis compensates for half the growth; explicit constraintH / constraintV: "scale" keep the offset proportional when the cell changes size. clipsContent: false allows the card to extend beyond its cell; also check clipping on outer containers.
The two axes' variables and aspect locking belong to different layers here. This does not bypass Figma's constraints on a single node. The inner component follows width: if independent limits give the cell a different ratio, the content may extend beyond its height. This recipe does not promise automatic fitting on both axes.
Updates
Release history for the Composition type. The newest release is on top; open an entry to see what changed.
2.9.2Latest
- The
effectskey of composition styles accepts every Figma effect layer: progressive blur, noise, texture, glass and shaders, with the same keys as tokens of theeffectstype. - Re-applying a regular component no longer triggers the variant-set unwrap scenario (2.9.0).
2.8.10
- Root
$descriptionMarkdownanddocumentationLinksround-trip through Generate, Apply, and Get Code. - A documentation link may be authored as a bare HTTP(S) URL or one whole-string Markdown link; SXL Studio sends its bare destination URL to Figma.
- Get Code now requires a unique portable source identity and preserves author-owned structure and ref descriptions.
2.7.7Previous
ref.stylesapplies normalized styles to the referenced instance root inINSTANCErefs and reference-modeSLOTdefaults.refStylesin a style block provides variant-specific overrides for that referenced root without styling the abstract slot host or preferred swaps.
2.7.1Previous
- Compact
statesaxes for mutually-exclusive states such asloadinganddisabled— no more full cartesian blow-up. - Unified Composition Grid placement model shared by preview and generated component sets.
VECTORshapes for custom silhouettes, including token-drivenvectorCornerRadii.- Variant-specific
refoverrides for wrapper sets that contain one nestedINSTANCE. - Clearer
displayvsvisiblebehavior for conditional branches. - Faster
Generate/Applyin large files, especially for slot- and instance-heavy sets.
Where It's Used
Use Composition whenever you need predictable, repeatable component building in a design system.
Common use cases:
- generate new components / component sets from a token file;
- update existing components without rebuilding them (
Apply); - keep variant structure, slot content, and styling rules in one source;
- hand a deterministic component model to engineers and agents.
How SXL Studio works with it
- You author JSON manually or bootstrap it from Get Code.
- SXL Studio parses and validates the file.
Generatecreates a new component/set, orApplyupdates tracked nodes.- Tracking is stored in
diff-id.jsonso updates stay stable between runs.
React / Vue 3 transformation
Composition JSON is machine-readable and stable enough for custom transformation scripts. The plugin uses it for Figma generation, and you can read structure + styles to map a component to your framework. In Dev Mode, SXL Studio also produces dedicated codegen output (including Vue 3) you can use as a reference.
Generate
Generate builds a new component or component set from the JSON: it reads structure, expands props into variants, applies styles, and resolves token references ({path.to.token}) at build time. Use it for a component that does not exist in Figma yet.
Pre-check before Generate / Apply
Fix JSON editor errors before running. Typical causes:
- unresolved token references (
{path.to.token}); - invalid alias syntax;
- schema violations (for example an invalid
SLOTshape).
Recommended flow: open JSON → resolve all validation issues → save → run Generate or Apply.
Get Code
Get Code reads the selected Figma node(s) and generates a Composition JSON draft — a fast starting point you then refine by hand. A semantically empty editor stub can be bootstrapped from a supported single selection or multi-selection; multi-select output may contain $synthetic: true to mark a synthetic set construction (informational only).
Refreshing a non-empty Composition is deliberately stricter. Its portable $extensions["sxl.studio"].composition identity must resolve uniquely to the selected Component or ComponentSet; selecting an Instance resolves through its main component root. A matching name alone is never accepted. Ambiguous or unreadable identity, a different composition root, multi-selection, or a plain Frame stops Get Code without changing the editor. A result is also discarded if the editor changes while Figma is exporting it.
Get Code keeps author-owned extensions on structurally proven nodes, including structure.description and ref.description. Generated structure and style fields remain authoritative, and both the generated snapshot and the final merged document must pass Composition parsing before they can replace editor content. An Instance selected from a ComponentSet is exported as a component: false snapshot without incompatible variant-level props or states.
Grid settings
Grid controls how variants are laid out on the canvas in a generated component set. It changes placement only — never token semantics or the JSON contract.
- Columns / Rows assign variant axes explicitly; multiple axes in one zone nest from outer to inner.
statesaxes are not placed in Columns / Rows — they render as a trailing state band below or to the right.- spacing is split by meaning: variant gap, prop/group gap, component-set padding, label gap, and state-band gap.
- grid annotations render outside the set on the same placement tracks, so labels match preview and output. After a set is moved, Apply moves its labels and removes the old labels on the other page. Repeated Apply keeps unchanged labels and repairs manual changes to their text or appearance.
Tip In large Figma files the same JSON stays stable while SXL Studio reduces redundant restyling for slot-heavy components. Keep class / name values stable on layers targeted by styles, and use states for mutually-exclusive states instead of expanding every boolean combination.
Apply
Apply updates an existing tracked component in place, using diff-id.json tracking so identity and instances are preserved.
The actions live in the context menu of the composition badge in the token editor and of the folder that holds compositions:
| Action | Purpose |
|---|---|
| Generate | Creates a new component or variant set from JSON |
| Smart Apply | Updates the tracked component in place, or creates a new one if it does not exist in the file yet |
| Repair Apply | Rebuilds the variants of the tracked component, restoring the structure after manual edits |
| Adopt composition | Links the JSON to an existing component selected on the canvas |
| Audit composition | Compares the component on the canvas with the JSON and shows the differences without changing anything |
Instance overrides during Apply
Instances of an updated main component keep their overrides (a swapped icon, edited text, boolean properties, bindings) because the update happens in place by layer identity. Apply verifies this instead of assuming it: before the main components change, the real overrides of their document instances are captured (from the native override metadata and the difference from the declared defaults; for nested layers, by layer path), and they are read back after the update and again after the finishing passes. A compatible override the update reset is written back and verified; inherited defaults are not overrides and follow the new JSON; a property or layer removed by JSON is an expected loss and is listed in the warnings. When a compatible override cannot be restored, the result becomes unsuccessful with no success stamps and the message names the instance and the property. The check covers the first 400 instances; beyond that a warning says the rest were not verified.
Result statuses and recovery
Generate, Smart Apply and Repair Apply report one of three statuses. Success (green check): the changes are applied and there is nothing to note; informational summaries (for example a consumer-override check that restored and lost nothing) do not change the status. Warning (yellow triangle): the changes are applied, but each notice is listed on its own line with a recommendation; where it helps, the notification offers a Repair button right away. Error (red cross in a circle): the operation did not complete; the items say what is wrong and how to fix it. For an error the notification offers Auto-fix: it re-links the existing variants to the composition, waits for Local Workspace, runs Repair Apply and then Smart Apply anchored to the root tracked under the composition name (which also settles the ambiguity of two files carrying one copied identity), and shows the result with the same three statuses. Auto-fix never edits JSON, so the button is absent only for pure authoring conflicts — a locked ratio with variables on both axes, an unresolved reference, a migration without a map, an invalid composition file: those stay errors with a recommendation for the JSON.
Notices and errors are rendered as a list. Each item carries a severity glyph: a red cross for an error, a yellow triangle for a notice, a grey “i” for an informational line; errors come first, then notices. A long message is broken into a headline and separate “what is wrong” lines (for example every conflicting width/height pair on its own line), and the fix is listed as separate steps, each with a “→” arrow; layer names, properties and {…} tokens are emphasised. When several items share the same set of steps, it is shown once above the list. The headline carries the count (“N errors · M notices”), a list longer than eight items collapses to eight with a Show N more button, and Copy all copies the items in the same shape: headline, lines and steps. The same list is used by the notifications of the variables and styles export, Apply Tokens and Apply Data.
The Bridge commands generate_composition, apply_composition and auto_fix_composition return the same data in status, diagnostics (severity, code, message, details — facts one per line, fixes — the steps in order, recommendation — the same as one string, action) and suggestedAction.
Saving a token file through the plugin puts Local Workspace into a reload for a few seconds. Generate and Apply wait for it before writing, and the tracking write after a canvas change is retried once the workspace is ready again, so that window no longer leaves an operation incomplete. When the workspace demands a reload (a Bridge write whose outcome was not confirmed), Generate and Apply stop before any change with a readiness error; the reload is run by the banner's Reload button or by the reload_local_workspace command for agents — the same re-read from disk, editor drafts untouched. A disk change that lands while an operation is running is applied at that operation's readiness wait rather than after its timeout. A conflict between an unsaved editor draft and disk is not cleared by waiting: the operation stops and names the reason, and reload_local_workspace refuses with the same reason — the decision is the user's, in the Tokens panel.
Changing a layer kind: FRAME ↔ SLOT
When a FRAME layer in structure gets the SLOT tag, Apply on a standalone component replaces it with a native slot in the same position (nested layers move inside and keep their ids; the container's own id changes, because only createSlot() creates a slot together with its property). Inside a variant set the layer stays a container in place (its id is kept): the set receives a new slot property and the slot-binding pass attaches the container to it. The reverse change SLOT → FRAME rebuilds the layer as a plain frame and removes the slot. Stand-ins that are already bound, and layers whose slot property of the same name already exists, are not recreated.
INSTANCE_SWAP properties and per-variant references
The default of an INSTANCE_SWAP property comes from componentProperties.<name>.defaultValue in JSON when it resolves to a component; the component currently on the layer is only the fallback. A layer bound to such a property shows the default on every variant: a per-variant component swap for that layer ("$size=md .root": { ".swap": { "component": "…" } }) cannot be represented in Figma and is reported as a warning. For a layer without an INSTANCE_SWAP binding the per-variant component swap is applied by Generate, Smart and Repair, and the audit expects the same.
Unresolved references
When an INSTANCE / ICON layer's reference cannot be resolved (no library access, wrong key, an unexported variable in a property), the Generate or Apply result is an error with the primary reason, not a successful update: no success stamps are written, the layer stays an empty marked stand-in, and instance-only operations such as scale are not attempted on it. The node at that path keeps its id and overrides while the reference is unresolved: a repeated failing Apply does not create another stand-in. Once the reference is fixed, the next Apply replaces the layer with the instance, keeping the set, the main components and the sibling layers. JSON is the source of truth for the managed main component: a layer of another type at the authored instance path is brought to JSON together with its content; instance overrides are a separate level and are not reset by this.
Restoring bindings in Smart Apply
In the upcoming 2.9.5 version, Smart Apply checks direct bindings on managed layers both when JSON is unchanged and after an unrelated style block changes. Supported numeric fields, visibility, colour variables in solid fills and strokes, and uniform text fields can recover a detached or incorrect variable alias. Literal ref.properties values of the main component's own nested instances (text, boolean, variant) are compared with the live values as well: when a value drifted — a property renamed on the source component, a manual edit inside the main component — Smart writes the JSON value back.
For a native slot's default instance, only explicitly authored styles are restored, with the previous main component and default-content properties verified. Manual content replacements and changes to its component properties are preserved. Internal instance layers that inherit appearance from another main component do not become managed layers of the parent composition.
This binding check does not reset PaintStyle / TextStyle links, mixed text-range formatting or selected variable modes. It does not replace a full structural Repair. Ordinary application of an explicit JSON change still follows that field's contract: for example, authored fontSize may override a TextStyle.
Target resolution and preservation
Every selected composition root is processed with its own explicit anchor. SXL Studio resolves the complete selection before the first canvas mutation, so a chain such as sm → md and md → lg cannot make the first rename steal the second JSON file's target.
- A unique live identity updates that Component or ComponentSet in place. Its Figma node ID stays stable, so existing instances keep their component link.
- Smart Apply with a stale or foreign link and no live candidate in the current file creates a new component.
- An explicit Apply without a matching target asks you to select the intended root or use Generate. Ambiguous, conflicting, or incompletely scanned candidates stop before mutation.
- Apply preserves consumer overrides. It does not blanket-reset text, component properties, instance swaps, or properties the composition does not author.
- Generate and Apply from the plugin UI do not require Bridge. Remote Connect execution and Git Sync Local Storage still use Bridge for their own transport/storage contracts.
SXL Studio manages a portable source identity under $extensions["sxl.studio"].composition. Older JSON without this block remains supported; a successful Generate or anchored Apply attempts to backfill it without rewriting unrelated $extensions:
{
"$extensions": {
"sxl.studio": {
"composition": {
"id": "11111111-1111-4111-8111-111111111111",
"roots": [
{
"nodeId": "21649:1480",
"key": "figma-component-key",
"type": "COMPONENT_SET"
}
]
}
}
}
}
This block is managed metadata, not an authoring input. If storage is temporarily unavailable, the canvas operation stays successful and reports a warning; identity persistence is retried on a later run.
Adopt & Audit existing components
A component created manually before Composition JSON existed does not need to be deleted. Use the composition badge context menu:
| Action | What it does | When to use it |
|---|---|---|
Adopt composition | Links the selected component/set to the JSON without rebuilding layers. | When the Figma component exists and future Apply should update it. |
Audit composition | Compares the selected/linked component against the JSON and reports sync status. | Before adopting a legacy component, after manual edits, or before repair. |
Adopt is conservative: it keeps component identity (instances elsewhere keep their link), checks that variant names match the JSON matrix, and does not rewrite layers, styles, or properties until an audit/apply confirms the real structure.
Root fields
The top-level keys of a composition file.
| Field | Type | Required | Notes |
|---|---|---|---|
$type | "composition" | ✅ | File type marker |
name | string | ✅ | Component / component-set name |
structure | node | ✅ | Layer tree |
styles | object | ✅ | Base styles + selector rules |
props | object | — | Variant axes |
states | string[] | — | Mutually-exclusive axes taken from props |
componentProperties | object | — | Native Figma properties |
transitions | object | — | Prototype transitions (transition singular accepted as a legacy alias) |
themeBinding | object | — | Variant prop → variable-collection modes |
component | boolean | — | Default true; false builds plain nodes and forbids non-empty props |
$description | string | — | Figma description written on the root component/set |
$descriptionMarkdown | string | — | Rich Figma description written on the root component/set |
documentationLinks | { uri: string }[] | — | Zero or one root documentation link; bare URL or one Markdown link |
$metadata | any | — | Opaque tooling metadata; ignored by generation |
size / style | object | — | Optional embedded token blocks (see Advanced fields) |
selectors | auto | auto | Generated by the parser from styles keys — do not author manually |
Warning Removed legacy keys error on parse: adapters, sizeStyles, colorStyles. And component: false is incompatible with non-empty props.
Root descriptions and documentation link
$descriptionMarkdown contains the rich component description and may use normal Markdown. documentationLinks is a separate Figma Link field and supports at most one item. Its uri accepts either a bare absolute HTTP(S) URL or one Markdown link occupying the whole string:
{
"$descriptionMarkdown": "Read the [Popover documentation](https://docs.sxl.team/ds/components/popover).",
"documentationLinks": [
{
"uri": "[Popover documentation](https://docs.sxl.team/ds/components/popover)"
}
]
}
SXL Studio normalizes the second form to https://docs.sxl.team/ds/components/popover before writing it to Figma. Relative URLs, unsafe schemes, surrounding prose, multiple Markdown links, and Markdown titles are rejected. The authority is ASCII-only; use punycode for internationalized domain names. An omitted annotation field preserves the existing manual value during Apply or an up-to-date Generate; "" clears the description and [] clears the Link.
Structure
structure is the layer tree the generator builds. Every node needs a tag and a class; everything else is optional.
Node fields
| Field | Required | What it does | Figma equivalent | Frontend equivalent |
|---|---|---|---|---|
tag | ✅ | Node kind to create | Layer type | Element type |
class | ✅ | Style/target binding key | Layer selector | className |
name | — | Explicit Figma layer name | Layer name | data-name / label |
layer | — | Alias for name when name is unset | Layer name | data-name |
content | — | Text content (mainly TEXT) | Text value | text node / children |
description | — | Human note, no visual effect | Layer description | code comment |
ref | — | Component to instantiate + config | Instance → main comp. | imported component |
slot | — | Native slot config (reference mode) | Slot property | <slot> / children |
children | — | Nested child nodes | Nested layers | child elements |
vectorPaths | — | SVG-like path data (VECTOR only) | Vector paths | <path d="…"> |
vectorNetwork | — | Editable vertex geometry (VECTOR) | Vector network | — |
viewBox | — | [x, y, w, h] coordinate box (VECTOR) | — | SVG viewBox |
"structure": {
"tag": "FRAME",
"class": "root",
"name": "Root",
"children": []
}
Per-field JSON examples:
{ "tag": "FRAME", "class": "card" }
{ "tag": "TEXT", "class": "label", "name": "Label", "content": "Continue" }
{
"tag": "INSTANCE",
"class": "icon",
"ref": { "component": "circle-info", "properties": { "size": "md" } }
}
{
"tag": "SLOT",
"class": "content-slot",
"slot": { "default": "WButton", "preferred": ["WButton", "WChip"] }
}
{
"tag": "FRAME",
"class": "row",
"description": "Header row",
"children": [{ "tag": "TEXT", "class": "title", "content": "Title" }]
}
Note class is the style key, name is the Figma layer name. For nested targeting (ref.nested, NESTED_INSTANCE), always set an explicit, stable name.
Supported tag values
| Tag | What it creates | Figma equivalent | Frontend equivalent | Constraints |
|---|---|---|---|---|
FRAME | Container / auto-layout frame | Frame / Auto Layout | <div> (flex container) | — |
TEXT | Text layer | Text | <span> / <p> | value comes from content |
COMPONENT | Nested main component node | Component | component definition | — |
INSTANCE | Instance from ref | Instance | <Component /> usage | needs ref; placeholder if unresolved |
ICON | Icon instance resolved from ref | Icon instance | <Icon /> | like INSTANCE; color recolors the glyph |
SLOT | Native slot (or fallback) | Slot | <slot> / {children} | reference or children mode, not both |
RECTANGLE | Rectangle shape | Rectangle | <div> block | shape paint via fill |
ELLIPSE | Ellipse shape | Ellipse | border-radius: 50% div | — |
LINE | Line shape | Line | <hr> / divider | — |
VECTOR | Custom vector from path data / vector network | Vector (Pen) | inline <svg><path> | not a raw SVG import |
Per-tag JSON examples:
{ "tag": "FRAME", "class": "card", "children": [] }
{ "tag": "TEXT", "class": "label", "content": "Continue" }
{ "tag": "COMPONENT", "class": "chip" }
{
"tag": "INSTANCE",
"class": "cta",
"ref": { "component": "WButton", "properties": { "variant": "primary" } }
}
{
"tag": "ICON",
"class": "leading-icon",
"ref": { "component": "circle-info" }
}
{ "tag": "SLOT", "class": "content-slot", "slot": { "default": "WButton" } }
{ "tag": "RECTANGLE", "class": "bg" }
{ "tag": "ELLIPSE", "class": "avatar-mask" }
{ "tag": "LINE", "class": "divider" }
{
"tag": "VECTOR",
"class": "body-shape",
"vectorPaths": [
{ "data": "M 0 140 L 564 140 L 540 0 L 24 0 Z", "windingRule": "NONZERO" }
],
"viewBox": [0, 0, 564, 140]
}
Use VECTOR when a shape is not a rectangle, ellipse, or line — a slanted button body, a wave, a tab notch, a custom badge. It accepts SVG-like vectorPaths or a Figma-like vectorNetwork for editable vertices, and takes the same visual styles as other shapes. Vertex rounding can be tokenized from styles via vectorCornerRadii.
Slanted button pattern
Keep editable shape geometry in structure, and drive color/size/rounding from styles. The middle stays a normal auto-layout FRAME so text and icons behave normally.
{
"tag": "FRAME",
"class": "wrap",
"children": [
{
"tag": "VECTOR",
"class": "left-shape",
"vectorNetwork": {
"vertices": [
{ "x": 18, "y": 56 },
{ "x": 0, "y": 56 },
{ "x": 10, "y": 0 },
{ "x": 18, "y": 0 }
],
"segments": [
{ "start": 0, "end": 1 },
{ "start": 1, "end": 2 },
{ "start": 2, "end": 3 },
{ "start": 3, "end": 0 }
],
"regions": [{ "windingRule": "NONZERO", "loops": [[0, 1, 2, 3]] }]
}
},
{
"tag": "FRAME",
"class": "content",
"children": [{ "tag": "TEXT", "class": "label", "content": "PRIMARY" }]
}
]
}
".left-shape": { "widthType": "fixed", "heightType": "fixed", "width": 18, "height": 56, "fill": "{button.bg.accent}", "vectorCornerRadii": [0, "{radius.md}", "{radius.md}", 0] }
vectorCornerRadii accepts an array (by vertex order) or an object ({ "1": "{radius.md}" }). Vertex indexes are zero-based. VECTOR is not a raw SVG import — put curves, skew, and cut-outs into vectorPaths / vectorNetwork.
SLOT modes and rules
SLOT has strict modes: reference mode (a slot config with an optional ref template) or children mode (inline children). A reference config cannot be combined with non-empty children. For an empty slot without a reference config, set children: []; omitting both forms is invalid.
{
"tag": "SLOT",
"class": "content-slot",
"slot": { "default": "WButton", "preferred": ["WButton", "WChip"] },
"ref": { "component": "WButton", "properties": { "size": "md" } }
}
{
"tag": "SLOT",
"class": "rows",
"children": [{ "tag": "FRAME", "class": "row", "children": [] }]
}
Constraints: a SLOT cannot have both slot and non-empty children; SLOT.ref is only valid in reference mode. A SLOT with exactly one INSTANCE child (with ref, no slot) is treated as reference-mode shorthand.
Note The canonical, schema-portable form for both slot.preferred and componentProperties.*.preferred is an array of component names. Apply also accepts Figma API-derived entries such as { "type": "COMPONENT_SET", "key": "..." }; an extra display label such as component is ignored. Use component names when the file must validate against a string-only external schema.
ref for INSTANCE / SLOT
| Field | What it does | Example |
|---|---|---|
component | Target component name (required when ref exists) | "component": "WButton" |
library | Publishing library name (excludes local components) | "library": "SXL DS" |
key | Published component key (highest priority) | "key": "abc123" |
properties | Primitive instance property values | "properties": { "state": "active" } |
styles | Styles for the referenced instance root | "styles": { "width": "{size.icon}" } |
iconBindProperty | Explicit property name for icon swap | "iconBindProperty": "icon" |
icon | Nested icon swap descriptor | "icon": { "component": "fire-3" } |
overrides | Child content overrides | "overrides": { "label": "Apply" } |
slots | Slot overrides on the referenced instance | see below |
nested | Nested instance updates by layer name | see below |
description | Optional note | "description": "Primary CTA" |
key takes priority: the component is imported by its published key, even if a local component has the same name. If import fails, Apply reports an unresolved reference instead of choosing a local component.
library excludes local components. Lookup first uses a key remembered for that library, then other remembered published keys and remote components already used on the canvas. When several saved sources have the same component name, a matching library name takes priority; unresolved ambiguity produces a warning asking for key. Library names ignore case, spaces and punctuation, so "Design Library: Core" and "Design Library Core" match. Component names remain exact. Without publishing metadata, the library name is a lookup hint; use key when exact identity is required. Enabling a library alone may not make its components discoverable through Figma's plugin API. These rules also apply to ref.nested.<layer>.component and icon swaps. Without library or key, local name lookup remains available.
For compositions that create components, Apply also checks tracked instance references when the composition has not changed. If a local main was deleted, Apply can reconnect the existing instance to a unique live component whose saved tracking confirms the same JSON source. A matching name alone is insufficient. Healthy links and manual component swaps remain intact, and key/library restrictions still apply. If the source, target or preservation of manual content cannot be verified, Apply reports an incomplete run without guessing a replacement. Manual component-property overrides currently require manual recovery. Lost slot defaults with authored ref.properties overrides also require manual recovery for now. This does not add a local node-ID selector to JSON.
ref.styles is for the materialized referenced INSTANCE root. Use it when the default instance itself needs layout or paint bindings, for example an icon size binding inside a SLOT. It supports the same style aliases and token refs as normal styles, including widthType, heightType, width, height, fill, and color.
The host style block still styles the composition node itself. In reference-mode slots, use refStyles inside a normal style block when variants need to override the referenced root. refStyles merges over ref.styles and is applied only while the current slot child still matches ref.component; it is not cascaded to a preferred/user swap such as WBadge.
{
"tag": "SLOT",
"class": "leading-slot",
"slot": { "default": "circle-info", "preferred": ["circle-info", "WBadge"] },
"ref": {
"component": "circle-info",
"properties": { "style": "filled" },
"styles": {
"widthType": "fixed",
"heightType": "fixed",
"width": "{sz.fixed.reg.xs}",
"height": "{sz.fixed.reg.xs}"
}
}
}
"styles": {
"$placement=inner .leading-slot": {
"refStyles": {
"width": "{sz.fixed.reg.3xs}",
"height": "{sz.fixed.reg.3xs}"
}
}
}
Variant defaults in native slots
Available from version 2.9.4.
A reference-mode slot uses the default instance reference after the variant style cascade. This applies both to SLOT.ref with slot.default and to the shorthand with a single INSTANCE child. For the same referenced component, properties and styles merge by individual key. Changing component, key, or library starts a new reference: restate the target identity and any properties or styles it needs, rather than inheriting the previous component's settings.
For example, a Modal can use a compact header and a Drawer a wide header. Give the header slot a default instance, then set the header properties and root styles for each parent variant. Keep the parent layout in the parent style block; use the referenced instance's styles for the header itself.
Generate, Apply and template-based generation use that effective default. Apply updates the existing default instance in place only when it can identify it as the previously authored default. A manually replaced instance, changed authored properties, custom children or an empty slot are preserved with a warning. An interrupted update can be retried; Apply does not reset the slot to discard user content.
Edit slot settings
Declare description, preferred and slotSettings on the SLOT entry in componentProperties, not in structure.slot. Settings apply to reference slots, single-instance shorthand and children slots alike. An intentionally empty slot needs explicit children: []; a bare SLOT is invalid.
{
"$type": "composition",
"name": "Panel",
"componentProperties": {
"content": {
"type": "SLOT",
"layer": "content",
"description": "Panel content",
"preferred": ["Header"],
"slotSettings": {
"stretchChildOnInsert": true,
"displayEmptyByDefault": false,
"minChildren": 0,
"maxChildren": null,
"allowPreferredValuesOnly": false
}
}
},
"structure": {
"tag": "FRAME",
"class": "root",
"children": [{ "tag": "SLOT", "class": "content", "children": [] }]
},
"styles": { ".root": { "direction": "column", "width": 320, "height": 240 } }
}
To insert a default instance, replace the slot's empty children array with [{ "tag": "INSTANCE", "class": "header", "ref": { "component": "Header" } }]. The default content belongs in the structure; componentProperties.content.defaultValue is optional legacy input, not an Edit slot setting.
The three boolean settings require JSON booleans. minChildren and maxChildren accept non-negative integers or null to clear a limit; when both are numbers, the minimum must not exceed the maximum. preferred: [] explicitly clears the preferred list. Unknown setting names are rejected. These are Figma insertion hints and advisory limits, not permission to remove existing content or a destructive validation rule. See Figma SlotSettings.
The plugin saves these settings in Figma. Automatic stretching depends on the current Figma version and may not take effect when stretchChildOnInsert is enabled. displayEmptyByDefault controls the highlight of an empty slot; it does not remove its default content.
In variant styles, put ref on the SLOT.class selector for a direct reference slot, or on the declared child class for the single-instance shorthand. A ref targeting an implicit path derived from the default component name is ignored with a warning to protect user changes. Ordinary width, colour and layout styles can still target that path.
The JSON key under componentProperties defines the authored property name. To rename it, change this key while keeping layer and the slot node in structure. For example, replace content with body:
"componentProperties": {
"body": {
"type": "SLOT",
"layer": "content",
"description": "Panel content"
}
}
Apply renames the existing native property and preserves its ID, binding and slot content. The layer name in structure does not need to change. Unchanged JSON preserves a manual name in Figma. Name conflicts or unverified bindings produce diagnostics and leave Apply incomplete.
If another composition addresses this slot through ref.slots or ref.nested, update references that use the old slot name or full property key. These references select the current native property; renaming its owner does not rewrite other JSON files.
When upgrading an older composition: first apply the original JSON so the plugin records its authored name, then change the property key. Initial matching preserves the current Figma name: without prior history, the plugin cannot distinguish a manual rename from a JSON edit. Descriptions, preferred components and settings can be updated independently of the name.
Get Code preserves slot descriptions, settings and published preferred-component keys in JSON. Physically empty slots export as children: []; hidden default nodes retain visible: false.
Native slots inside nested instances
Available from version 2.9.4.
Use ref.nested.<label>.slots to set one default component inside a nested instance's native slot. The nested target must have a unique exact layer name. If names repeat, provide path as an array of exact direct-child names, relative to the referenced root. The label then identifies the JSON block; path selects the instance.
"nested": {
"field": {
"path": ["body", "field"],
"slots": {
"leadingSlot": {
"component": "PhonePrefix",
"properties": { "size": "lg" }
}
}
}
}
A slot is selected by its exact native property key, its unique property name, or its unique layer name when no such property exists. Full property keys disambiguate identical labels. Ordinary frames and slots belonging to another nested instance are not substitutes. Published content may also specify library and key.
The nested path supports one component, or the equivalent nodes: [{ "component": "PhonePrefix" }]. Explicit {"op":"replace","nodes":[]} clears only content proven to be the authored default. Omitting slots or an individual slot preserves its contents. Manual swaps, changed properties, custom content, hidden layers and manually emptied slots are preserved. Initial adoption requires a match with the main component's slot default. This match is checked before the same block updates BOOLEAN properties that control slot visibility, then verified again before content changes. A previously changed slot does not gain ownership merely because a later property update makes it look like the default. Unavailable components or ambiguous targets produce diagnostics and leave the run incomplete.
append, patch, multiple children, child name and child icon are not supported in this nested contract and cause validation errors. The top-level ref.slots contract is separate. Nested slot updates run after parent property updates. Generate builds affected variants individually; Apply updates existing nodes in place. Native slots are never detached, cloned into ordinary frames, reset, or assigned through setProperties.
Published component-set keys
ref.key accepts a published component key or component-set key. A set key selects a variant using the declared variant axes in properties; without variant selectors it uses the set's default variant. A concrete component key preserves that component unless variant selectors request another exact variant within the same set; axes omitted from properties retain the keyed component's values. Text, boolean and other instance properties are applied separately. Invalid or ambiguous variant combinations fail with a diagnostic; they do not fall back to a similarly named component. Remembered keys and ledger keys use the same component/set import path.
Nested references and slot overrides
ref.nested — addresses nested instances by layer name. component swaps the nested instance itself to another component (like a manual instance swap in Figma), properties sets its properties, icon swaps the icon inside it:
"nested": {
"Badge": {
"properties": { "label": "3" },
"icon": { "component": "fire-3", "properties": { "style": "filled" } }
},
"icon": { "component": "casino-games" }
}
ref.slots — override slots inside the referenced instance (op: replace, append, patch):
"slots": {
"footer": {
"op": "replace",
"nodes": [{ "component": "WButton", "name": "apply", "properties": { "variant": "primary" } }]
}
}
A component-set variant can wrap a different existing component by overriding its ref from a variant selector, so one INSTANCE node serves the whole wrapper set:
"styles": {
"$item=neutral-secondary-sm .item": {
"ref": { "component": "WButton", "properties": { "style": "neutral", "variant": "secondary", "size": "sm" } }
}
}
Props
| Key | Type | What it does |
|---|---|---|
props | Record<string, (string | boolean | number)[]> | Declares each variant axis and its allowed values |
states | string[] | Names of props axes that are mutually-exclusive states |
props declares every allowed value for each axis; each combination becomes one generated variant.
"props": {
"size": ["sm", "md", "lg"],
"state": ["default", "hover", "active"],
"compact": [true, false]
}
Behavior: selector matching compares values as strings; an axis with default uses it as fallback for missing value-specific rules, otherwise the first value is the fallback. Keep the axis count intentional — variant combinations grow multiplicatively.
states (mutually-exclusive axes)
states lists axes from props that behave like mutually-exclusive UI conditions (:disabled, :loading) rather than combinable variants. They are excluded from the cartesian product — each non-default value adds a single variant on top of the defaults.
"props": {
"_state": ["default", "hover", "focus", "select"],
"loading": ["false", "true"],
"disabled": ["false", "true"]
},
"states": ["loading", "disabled"]
Without states this is a 4 × 2 × 2 = 16-variant matrix; with states it compacts to 6 (4 interaction variants + loading=true + disabled=true). Rules: the off value is the axis's first value; every name in states must exist in props; Apply re-compacts an existing set declaratively. Keep normal visual choices (size, variant, tone, theme) in props only.
Codegen mapping:
- React / Vue: an axis whose options are exactly
false/truebecomes abooleanprop (e.g.loading?: boolean); - DivKit: component sets are emitted through
card.states/state_id;statesonly controls which variants are materialized, it does not generate a fakecustom_type; - a
_-prefixed axis (e.g._state) is an internal interaction-state axis, excluded from the public codegen API (the consumer's own:hover/:focusCSS drives it); non-_axes likeloading/disabledare emitted as real props.
The extended object form with combineWith (crossing a state with structural axes) is reserved but not yet implemented — using it raises a validation error.
Component Properties
componentProperties defines native Figma properties exposed on instances.
| Type | layer refers to | defaultValue |
|---|---|---|
TEXT | class in structure | Required; auto-derived from text content if omitted |
BOOLEAN | class in structure | Required; defaults to true if omitted |
INSTANCE_SWAP | class in structure | Required |
SLOT | class in structure | Optional legacy input; default content comes from structure |
NESTED_INSTANCE | Nested instance layer name (not class) | Not required |
One example per type:
"title": { "type": "TEXT", "layer": "title", "defaultValue": "Promo title" }
"showMeta": { "type": "BOOLEAN", "layer": "meta", "defaultValue": true }
"icon": { "type": "INSTANCE_SWAP", "layer": "icon", "defaultValue": "circle-info", "preferred": ["circle-info", "star"] }
"media": { "type": "SLOT", "layer": "media-slot", "defaultValue": "WImageTile" }
"badge": { "type": "NESTED_INSTANCE", "layer": "Badge" }
layer rule: for TEXT, BOOLEAN, INSTANCE_SWAP, SLOT use the structure class; for NESTED_INSTANCE use the real nested Figma layer name.
Properties inside slots: Figma does not support binding an outer component property to an ordinary layer inside a native slot. For example, a text layer title inside a slot may keep its text and styles, but must not be the target of the outer componentProperties.title. Remove that property declaration and keep the text layer in place. A nested component instance can retain properties defined in its own main component. A visibility property on the slot itself is also supported. See Figma's slot documentation.
From version 2.9.4, validation warns about an authored property binding across this boundary, and Generate / Apply rejects it before changing the structure. If a required binding fails or cannot be read back, the run remains incomplete instead of reporting a successful Apply.
Styles
styles controls layer appearance, layout, and per-variant overrides. A key is a CSS-like class (.card), a legacy class (card), or a variant selector ($state=hover .card); blocks can be nested. Every property below is shown with its exact JSON spelling, grouped by purpose:
| Group | Covers |
|---|---|
| Container & auto-layout | direction, justifyContent, alignItems, gap, padding, … |
| Layer size | widthType, width, min/maxWidth, aspectRatio, … |
| Positioning | position, top/right/bottom/left, x/y, alignSelf, … |
| Grid | rows/columns, rowGap/columnGap, grid-child align/span |
| Background/fill/color | background, fill, color |
| Borders & corners | border, borderRadius, strokeAlign, cornerSmooth, … |
| Shadows & effects | boxShadow, backgroundBlur, layerBlur, opacity, glass |
| Typography | fontFamily, fontSize, lineHeight, textCase, … |
| Visibility & reset | visible, display, none-clearing |
| Instance control | component, instanceProperties, nestedInstanceProperties, ref |
| Variables & metadata | explicitVariableModes, layoutGrids, exportSettings, mask |
"styles": {
".root": { "direction": "row", "gap": 8, "padding": "12 16" },
"$state=hover .root": { "background": "{color.brand.hover}" }
}
Selectors and cascade
- base styles apply first; matching selectors apply by specificity (more conditions win);
- same specificity → later JSON declaration wins;
- descendant selectors resolve through dot paths (
.footer .item→footer.item); - when a class repeats in different branches, prefer a full path (
.header .item) over a bare.item; - a selector key may mix conditions and classes in any order (
"$state=hover .root"equals".root $state=hover"), and nested blocks inside it accumulate both:"$variant=secondary .root": { "background": "…", ".label": { "color": "…" } }yields the rules$variant=secondary .rootand$variant=secondary .root .label.
"styles": {
".wrap": {
"padding": 8,
".item": { "widthType": "fill" },
"$state=active": { ".item": { "opacity": 1 } }
}
}
Container and auto-layout
| Property | What it does — values | Example |
|---|---|---|
direction | Layout axis — row, column, grid, none | "direction": "row" |
justifyContent | Main-axis distribution — start, center, end, space-between, space-evenly, space-around | "justifyContent": "space-between" |
alignItems | Cross-axis alignment — start, center, end, baseline | "alignItems": "center" |
alignContent | Wrapped-rows distribution — auto, space-between | "alignContent": "space-between" |
flexWrap | Wrapping — nowrap, wrap | "flexWrap": "wrap" |
gap | Spacing between children (px) | "gap": 12 |
wrapGap | Cross-axis spacing between wrapped rows — px or "auto" (follows gap) | "wrapGap": 8 |
padding | Inner padding — number, "T R B L", or "none" | "padding": "16 16 20 16" |
overflow | Clipping — hidden, clip, visible, auto, scroll | "overflow": "hidden" |
primaryAxisSizingMode | Container main-axis sizing — auto, fixed | "primaryAxisSizingMode": "auto" |
counterAxisSizingMode | Container cross-axis sizing — auto, fixed | "counterAxisSizingMode": "fixed" |
boxSizing | Strokes counted in layout — border-box, content-box | "boxSizing": "border-box" |
canvasStacking | Sibling z-order — first-on-top, last-on-top | "canvasStacking": "first-on-top" |
".header": { "direction": "row", "justifyContent": "space-between", "alignItems": "center", "gap": 8, "padding": "12 16" }
flexWrap, alignContent and wrapGap describe a wrapping row, so they need "direction": "row". Set
"wrapGap": "auto" to let the gap between rows follow gap; give it a number to space the rows apart
on their own. When a layer keeps its direction but a full style sheet drops flexWrap, wrapGap or
alignContent, Apply returns them to the Figma defaults (no wrapping, the gap back in sync, auto
distribution).
".tags": {
"direction": "row",
"flexWrap": "wrap",
"widthType": "hug",
"maxWidth": 1276,
"gap": 10,
"wrapGap": "auto",
"alignContent": "space-between"
}
Layer size
| Property | What it does — values | Example |
|---|---|---|
widthType / heightType | Sizing mode — fixed, hug, fill | "widthType": "fill" |
width / height | Fixed size (px) | "width": 360 |
scale | Absolute scale of an explicit INSTANCE / ICON from 0.01 to 100; limits | "scale": 1.03 |
minWidth / maxWidth | Width bounds (px) | "maxWidth": 480 |
minHeight / maxHeight | Height bounds (px) | "minHeight": 40 |
aspectRatio | Lock ratio — "1/1", or "none" / false to unlock | "aspectRatio": "1/1" |
constrainProportions | Lock width/height proportions — true, false | "constrainProportions": true |
targetAspectRatio is an accepted alias of aspectRatio.
Positioning
| Property | What it does — values | Example |
|---|---|---|
position | Flow — relative, absolute, none | "position": "absolute" |
top / right / bottom / left | Absolute insets (px) | "top": 8 |
x / y | Absolute canvas coordinates (px) | "x": 24 |
alignSelf | Override cross alignment — inherit, stretch, start, center, end | "alignSelf": "stretch" |
flexGrow | Grow factor | "flexGrow": 1 |
rotate | Rotation (deg) | "rotate": 45 |
constraintH / constraintV | Figma constraints — left, center, right, scale, stretch | "constraintH": "center" |
".badge": { "position": "absolute", "top": 8, "right": 8 }
Grid layout
Set when direction is grid. "display": "grid" says the same thing when there is no direction.
| Property | What it does | Example |
|---|---|---|
rows / columns | Track counts; optional when a template names the tracks | "columns": 3 |
rowGap / columnGap | Track gaps (px) | "columnGap": 8 |
gridTemplateRows / gridTemplateColumns | Track sizes: fr, px, auto/hug (fit content), repeat(n, …); string or array | "gridTemplateColumns": "repeat(7, 1fr)" |
gridAutoFlow | Placement — manual (cells you set), row (next free cell, in layer order) | "gridAutoFlow": "manual" |
gridAutoRows | Let Figma add and remove rows with the children — true, false | "gridAutoRows": true |
gridRow / gridColumn | Cell of a grid child, counted from 1 | "gridRow": 2, "gridColumn": 4 |
justifySelf / gridAlignSelf | Grid-child align (H / V) — start, center, end, auto | "justifySelf": "center" |
gridRowSpan / gridColumnSpan | Grid-child span (tracks) | "gridColumnSpan": 2 |
repeat(7, 1fr) means seven tracks, so columns may be left out — the template defines the count. If
you write both and they disagree, Generate and Apply stop before touching the canvas and name the layer
and the fix. Without gridRow / gridColumn the children fill the grid in the order of structure.
gridRow and gridColumn go together on the same layer and need a parent with
"gridAutoFlow": "manual" — with "row" Figma places the children itself. "gridAutoRows": true puts
the row count in Figma's hands, so rows and the length of gridTemplateRows stop applying.
When the JSON has no gridTemplateRows / gridTemplateColumns, Figma keeps the track sizes it has and
follows the row and column counts. A full style sheet that keeps direction: "grid" but drops
rowGap / columnGap returns the gaps to 0.
A SLOT layer cannot be a grid — Figma does not allow it. Put "direction": "grid" on a frame inside the
slot instead.
".grid": {
"direction": "grid",
"gridTemplateColumns": "repeat(7, 1fr)",
"gridTemplateRows": "hug hug",
"columnGap": 12,
"rowGap": 12,
"widthType": "fill",
"heightType": "hug",
".card": { "widthType": "fill", "heightType": "hug" }
}
A grid whose columns use fr needs a width to share out, so keep the grid itself fill or fixed — a
hug width has nothing to divide. In a "component": false composition the root lands straight on the
page and has no parent to fill: give the root a fixed width and keep fill on the grid inside it.
Background, fill, color
| Property | What it does — values | Example |
|---|---|---|
background | Container fill — hex, gradient, layer array, or "none" / "transparent" | "background": "#FFFFFF" |
fill | Shape / vector / icon paint — hex or token, "none" clears | "fill": "{button.primary}" |
color | CSS-like currentColor (text + icon glyph) — hex or token, "none" | "color": "{text.primary}" |
color is CSS-like currentColor: on TEXT it sets the text fill; on ICON / icon instances it recolors the glyph (fill-based icons get a fill, stroke-based icons get a stroke). Token refs keep the variable binding; literal colors replace it. fill and strokes are lower-level — use them to target shape fills or a direct stroke. For a stroke-only icon, "fill": "none" keeps just the stroke paint.
Gradient background:
".hero": { "background": "linear-gradient(135deg, #7B5CFA 0%, #FA5CB4 100%)" }
Multi-layer background (image + overlay):
".media-slot": {
"background": [
{ "type": "image", "url": "https://images.unsplash.com/photo-1498050108023-c5249f4df085", "scaleMode": "FILL", "opacity": 0.72, "blendMode": "NORMAL" },
"rgba(0,0,0,0.24)"
]
}
Borders, stroke, corners
| Property | What it does — values | Example |
|---|---|---|
border | Shorthand "<width> <style> <color>"; "none" removes the stroke | "border": "1px solid #E7EAF1" |
borderColor | Stroke color | "borderColor": "#E7EAF1" |
borderWidth | Stroke weight (px) | "borderWidth": 1 |
borderStyle | Stroke style — solid, dashed | "borderStyle": "dashed" |
borderRadius | Corner radius (px) | "borderRadius": 20 |
cornerSmooth | Corner smoothing 0–1 (squircle) | "cornerSmooth": 0.6 |
strokeAlign | Stroke position — inside, center, outside | "strokeAlign": "inside" |
strokeCap / strokeJoin | Line cap / join style | "strokeCap": "ROUND" |
strokeTopWeight … strokeLeftWeight | Per-side stroke weight (px) | "strokeTopWeight": 2 |
dashPattern | Dashed stroke — array of dash/gap lengths | "dashPattern": [4, 4] |
outline / outlineOffset | Outside stroke + offset | "outline": "2px solid #0D6EFD" |
vectorCornerRadii | Per-vertex radii for a VECTOR (by vertex order) | "vectorCornerRadii": [0, 8, 8, 0] |
".card": { "border": "1px solid #E7EAF1", "borderRadius": 20 },
"$state=active .card": { "outline": "2px solid #0D6EFD", "outlineOffset": "2px" }
Shadows and effects
| Property | What it does — values | Example |
|---|---|---|
boxShadow | Drop shadows — array of shadow objects, or "none" | see block |
backgroundBlur | Background blur radius (px), or "none" | "backgroundBlur": 12 |
layerBlur | Layer blur radius (px), or "none" | "layerBlur": 8 |
opacity | Layer opacity 0–1, or "none" (= 1) | "opacity": 0.56 |
blendMode | Layer blend mode: normal, multiply, screen, overlay, darken, lighten, … | "blendMode": "multiply" |
glass | Glass effect, or "none" to clear | "glass": "none" |
effects | Full effects composite (token alias or layers) | "effects": "{shadow.md}" |
".card": { "boxShadow": [{ "x": 0, "y": 8, "blur": 24, "spread": 0, "color": "rgba(16,24,40,0.14)" }] },
"$state=hover .card": { "boxShadow": [{ "x": 0, "y": 14, "blur": 36, "spread": 0, "color": "rgba(16,24,40,0.20)" }] }
Typography
Set on a TEXT node.
| Property | What it does — values | Example |
|---|---|---|
fontFamily | Font family | "fontFamily": "Inter" |
fontWeight | Weight | "fontWeight": 600 |
fontSize | Size (px) | "fontSize": 18 |
lineHeight | Line height | "lineHeight": "24px" |
letterSpacing | Tracking | "letterSpacing": "0px" |
textAlign | Horizontal — left, center, right, justify | "textAlign": "left" |
verticalAlign | Vertical — top, center, middle, bottom | "verticalAlign": "center" |
textCase | Case — none, uppercase, lowercase, capitalize, small-caps | "textCase": "uppercase" |
textDecoration | Decoration — none, underline, line-through | "textDecoration": "underline" |
textSizing | Auto-resize — fixed, height-auto, auto, truncate | "textSizing": "height-auto" |
textTruncation | Truncation — disabled, ending (ellipsis) | "textTruncation": "ending" |
maxLines | Max lines before truncation | "maxLines": 2 |
paragraphSpacing | Space between paragraphs (px) | "paragraphSpacing": 8 |
paragraphIndent | First-line indent (px) | "paragraphIndent": 16 |
leadingTrim / verticalTrim | Trim line-box leading — CAP_HEIGHT, NONE | "leadingTrim": "CAP_HEIGHT" |
".title": { "fontFamily": "Inter", "fontWeight": 600, "fontSize": 18, "lineHeight": "24px", "textAlign": "left", "textSizing": "height-auto" }
The composite typography / font key applies a full text style at once.
Visibility and reset
visible: falsekeeps the layer in the variant but hides it in Figma;display: "none"omits the branch from the resolved variant;- if both are set,
display: "none"wins. When a later selector re-shows a branch, include the size/layout it needs; - any other
displayvalue re-shows the branch, and"grid"additionally turns on the grid layout when there is nodirection.
"$loading=true": {
".button": {
".label": { "visible": false },
".spinner": { "display": "flex", "visible": true, "width": "{button.size.icon}" }
}
}
CSS-like clearing values (write these to reset a property):
| Reset spelling | Effect |
|---|---|
"background": "none" / "transparent" | Clears fills |
"color": "none" / "fill": "none" | Clears text/currentColor/paints |
"boxShadow": "none" / "effects": "none" | Clears effects |
"padding": "none" | All paddings 0 |
"opacity": "none" | Resolves to 1 |
"mask": "none" / false | Clears the mask |
"aspectRatio": "none" / false | Unlocks the ratio |
Instance control
Set on an INSTANCE node.
| Property | What it does | Example |
|---|---|---|
component | Swap the main component (name-only swap) | "component": "WButton" |
instanceProperties / properties | Set instance property values | see block |
nestedInstanceProperties | Set nested instance props by layer name | see block |
ref | Full instance target override | see ref section |
".action-main": { "component": "WButton", "instanceProperties": { "variant": "primary", "size": "md", "label": "Continue" } }
".badge": { "nestedInstanceProperties": { "Badge": { "label": "2" } } }
Variable modes and Figma metadata
| Property | What it does | Example |
|---|---|---|
explicitVariableModes | Force a variable mode per collection | "explicitVariableModes": { "Themes": "Dark" } |
variableModes | Alias of explicitVariableModes | "variableModes": { "Themes": "Dark" } |
layoutGrids | Figma layout grids array | see block |
exportSettings | Figma export settings array | see block |
mask / maskType | Layer mask — alpha, vector, luminance, none | "mask": "alpha" |
".root": {
"explicitVariableModes": { "Themes": "Dark" },
"layoutGrids": [{ "pattern": "GRID", "sectionSize": 8, "color": { "r": 0, "g": 0, "b": 1, "a": 0.12 } }],
"exportSettings": [{ "format": "PNG", "suffix": "@2x", "constraint": { "type": "SCALE", "value": 2 } }]
}
Clear an explicit mode for a collection with null, false, "none", "auto", or "unset".
Transitions
Prototype transitions are defined at the composition root (shorthand or object form), never as a layer style key. Apply rewrites only the reactions created from transitions; reactions added by hand in Figma stay.
| Field | What it does | Values |
|---|---|---|
trigger | When it fires | on-hover, on-click, on-press, on-drag, on-enter, on-leave, mouse-up, mouse-down, after-timeout: 500 (shorthand: after-timeout 500 dissolve 300ms) |
animation | Transition type | smart-animate, dissolve, instant, scroll-animate, slide-in, slide-out, push, move-in, move-out |
duration | Length | e.g. 200ms |
easing | Curve | linear, ease-in, ease-out, ease-in-out, ease-in-back, ease-out-back, ease-in-out-back, gentle, quick, bouncy, slow, cubic-bezier(...), spring(...) |
direction | Optional direction | left, right, top, bottom |
condition | Optional variable gate | { "variable": …, "op": "==", "value": … } |
The rule key says which variant the transition leaves and which it enters: "$prop=a -> $prop=b" (several conditions separated by spaces are allowed: "$variant=primary $disabled=false -> $variant=secondary"). Axis values may contain hyphens and dots. A named key ("hover-in") is valid only in the object form with from and to.
Shorthand — order is trigger animation duration easing [direction]:
"transitions": { "$state=default -> $state=hover": "on-hover smart-animate 200ms ease-out" }
Object form with a condition (named key, from and to give the direction):
"transitions": {
"hover-in": {
"from": "$state=default",
"to": "$state=hover",
"trigger": "on-hover",
"animation": "smart-animate",
"duration": "200ms",
"easing": "ease-out",
"direction": "right",
"condition": { "variable": "motion.enabled", "op": "==", "value": true }
}
}
Theme binding
themeBinding maps a variant axis (usually theme) to variable-collection modes for correct light/dark rendering.
| Field | What it does | Example |
|---|---|---|
target | Binding target label | "target": "root" |
prop | Axis name from props | "prop": "theme" |
modes | Map of axis value → mode config | { "light": { "type": "local", "collection": "Themes", "mode": "Light" } } |
applyTo | Which domains to switch | ["variables", "textStyles", "effectStyles"] |
"themeBinding": {
"target": "root",
"prop": "theme",
"modes": {
"light": { "type": "local", "collection": "Themes", "mode": "Light" },
"dark": { "type": "local", "collection": "Themes", "mode": "Dark" }
},
"applyTo": ["variables", "typography", "textStyles", "effectStyles", "paintStyles"]
}
prop must exist in props. A mode source has type (local / library), collection, mode, and optional library. Each modes.<value> is either a flat source or a wrapped { primary, fallback[] }.
Advanced fields
Embedded size / style
A composition file can embed DTCG size and style token blocks for self-contained component packs (namespaced by component name). They are optional and do not affect layout on their own.
"size": { "md": { "height": "40px", "paddingX": "16px" } },
"style": { "primary": { "bg": "{color.brand.primary}" } }
Full example
A complete component set touching most features: multi-axis props, componentProperties, a SLOT and an INSTANCE, selector overrides, root transitions, and themeBinding.
{
"$type": "composition",
"name": "WPromoCard",
"$description": "Promo card with media slot, action and variant states.",
"props": {
"theme": ["light", "dark"],
"size": ["sm", "md"],
"state": ["default", "hover", "disabled"]
},
"states": ["disabled"],
"componentProperties": {
"title": {
"type": "TEXT",
"layer": "title",
"defaultValue": "Promo title"
},
"showMeta": { "type": "BOOLEAN", "layer": "meta", "defaultValue": true },
"media": {
"type": "SLOT",
"layer": "media-slot",
"defaultValue": "WImageTile"
}
},
"structure": {
"tag": "FRAME",
"class": "card",
"name": "Card",
"children": [
{
"tag": "SLOT",
"class": "media-slot",
"slot": { "default": "WImageTile" }
},
{ "tag": "TEXT", "class": "title", "content": "Promo title" },
{ "tag": "TEXT", "class": "meta", "content": "Meta" },
{
"tag": "INSTANCE",
"class": "cta",
"ref": {
"component": "WButton",
"properties": { "variant": "primary" }
}
}
]
},
"styles": {
".card": {
"direction": "column",
"gap": 12,
"padding": 16,
"background": "{card.bg}",
"borderRadius": 16,
"widthType": "fixed",
"width": 320
},
"$size=sm .card": { "width": 260 },
"$state=hover .card": {
"boxShadow": [
{
"x": 0,
"y": 12,
"blur": 32,
"spread": 0,
"color": "rgba(0,0,0,0.18)"
}
]
},
"$state=disabled .card": { "opacity": 0.56 },
".title": { "fontFamily": "Inter", "fontWeight": 600, "fontSize": 16 }
},
"transitions": {
"$state=default -> $state=hover": "on-hover smart-animate 160ms ease-out"
},
"themeBinding": {
"target": "root",
"prop": "theme",
"modes": {
"light": { "type": "local", "collection": "Themes", "mode": "Light" },
"dark": { "type": "local", "collection": "Themes", "mode": "Dark" }
},
"applyTo": ["variables", "textStyles", "effectStyles"]
}
}
Adaptation checklist: replace component names (WImageTile, WButton) with names from your library; create a Themes collection or drop themeBinding; start with Generate, then iterate with Apply; keep name stable on nested-targeted nodes.
For agents & transformers
A) Figma → Composition JSON
- Set
$type: "composition"and a stablename. - Build
propsonly from real variant axes; addstatesfor mutually-exclusive conditions. - For each node write a valid
tagandclass; add stablenamefor nested-targeted nodes. - For instances write
ref.componentand only valid primitiveref.properties. - For slots use reference mode (
slot) or children mode (children), never both. - Put all visual/layout rules into
styles(base + selector overrides). - Use root-level
transitionsandthemeBinding, not layer-level hacks. - Validate that referenced components/tokens exist.
B) Composition JSON → React / Vue
- Read
structureas the element tree; useclass/path keys for style resolution. - Resolve variant selectors (
$prop=value .class) against incoming props. - Respect branch visibility (
display: "none"). - Treat
ref.component→ component import,ref.properties→ passed props. - Treat
componentProperties,themeBinding,slotHostPipeline, and$figmaas design-time metadata unless your runtime supports them. - Preserve token refs (
{...}) or pre-resolve them through your token engine.
C) Pre-flight validation
structureexists and every node hastag+class;stylesexists and only contains object values;themeBinding.propexists inprops;- no removed legacy keys (
adapters,sizeStyles,colorStyles); - no SLOT mode conflict (
slotwith non-emptychildren).
Author key → Figma runtime key (reference)
| Author key | Runtime key |
|---|---|
direction | layoutMode |
justifyContent | primaryAxisAlignItems |
alignItems | counterAxisAlignItems |
alignContent | counterAxisAlignContent |
flexWrap | layoutWrap |
gap / wrapGap | itemSpacing / counterAxisSpacing |
widthType / heightType | layoutSizingHorizontal / Vertical |
alignSelf / flexGrow | layoutAlign / layoutGrow |
position | layoutPositioning |
top / right / bottom / left | insets → layout/position |
rows / columns | gridRowCount / gridColumnCount |
rowGap / columnGap | gridRowGap / gridColumnGap |
gridAutoFlow / gridAutoRows | gridItemsPositioning / gridAutoTracks |
gridRow / gridColumn | grid cell (counted from 1 → from 0) |
borderWidth / borderAlign | strokeWeight / strokeAlign |
borderRadius / cornerSmooth | cornerRadius / cornerSmoothing |
textSizing | textAutoResize |
textAlign / verticalAlign | textAlignHorizontal / Vertical |
rotate | rotation |
boxSizing / canvasStacking | strokesIncludedInLayout / itemReverseZIndex |
justifySelf / gridAlignSelf | gridChildHorizontalAlign / VerticalAlign |
Author value → Figma enum (reference)
| Property | Author → enum |
|---|---|
direction | row/column/none/grid → HORIZONTAL/VERTICAL/NONE/GRID |
justifyContent | start/center/end/space-between/space-evenly/space-around → MIN/CENTER/MAX/SPACE_BETWEEN/SPACE_EVENLY/SPACE_AROUND |
alignItems | start/center/end/baseline → MIN/CENTER/MAX/BASELINE |
widthType / heightType | fixed/hug/fill (aliases: auto→HUG, stretch→FILL) → FIXED/HUG/FILL |
position | relative/absolute/none → AUTO/ABSOLUTE/AUTO |
textSizing | fixed/height-auto/auto/truncate → NONE/HEIGHT/WIDTH_AND_HEIGHT; truncate = NONE + textTruncation: ENDING |
textCase | none/uppercase/lowercase/capitalize/small-caps → ORIGINAL/UPPER/LOWER/TITLE/SMALL_CAPS |
strokeAlign | inside/center/outside → INSIDE/CENTER/OUTSIDE |
mask | alpha/vector/luminance/none → ALPHA/VECTOR/LUMINANCE or isMask:false |
Notes: token references ({...}) are preserved and resolved at apply time; numeric and unit strings (px, rem, deg) are normalized to numbers where needed.
Limitations & warnings
- Unsupported property/node combinations are ignored.
- Third-party/library instance internals can be partially protected by Figma.
NESTED_INSTANCEandref.nestedrely on stable layer names.- Large
propsmatrices produce heavy variant counts — usestateswhere appropriate. - Figma Generate/Apply silently skips unknown style keys without blocking documented properties and aliases. Keep unknown keys only when another output, such as code generation, consumes them.
- Removed legacy keys error on parse:
adapters,sizeStyles,colorStyles.component: falseis incompatible with non-emptyprops.
Related pages
Variable-bound variants
Variant style sections with a ref block ("$state=hover .cell .envelope": { "ref": { "properties": { "_state": "hover" } } }) override only the properties they name: the {token} binding from the structure ref stays, and after every Apply stage each such binding is verified — losing it returns an error, not a success.
A direct VARIANT property on an explicitly declared instance can use an exported STRING token. Generate selects an initial variant, then binds the property to the variable; Apply uses the same binding contract. The value resolved for the consuming instance's active mode must exactly match the property's variant options. Other modes may contain values absent from those options; alias dependencies still require valid STRING values and identities. Missing identities, non-STRING variables, cycles and invalid values produce a diagnostic instead of a literal fallback. The variable identity comes from the workspace ledgers, not from a matching display name: first the active file's ledger, then the sibling diff-id.*.json ledgers of the same Local Workspace — for example when the STRING is exported to a Core library while the composition is built in a components file. A foreign variable is imported by its published key and verified by the returned key and type; the library must be published and available. The same token path exported to several files produces a conflicting-identity diagnostic.
{
"ref": {
"component": "ResponsiveContent",
"properties": {
"state": "default",
"breakpoint": "{breakpoint}"
}
}
}
For example, a shared token can contain desktop, tablet and mobile, while the instance exposes only desktop and mobile. It can bind directly in Desktop or Mobile mode without a duplicate token or Tablet variant. Apply reports an error if the consuming instance resolves to tablet; it does not substitute another variant. Switching to an unsupported mode later in Figma is still unsupported. Generate selects the initial variant from the value resolved for the placement (the parent layer's inherited or explicit mode) and, without such context, from the collection default mode; that value must be one of the variant options, otherwise the diagnostic names the actual value. The actual consuming instance is validated before binding. The outer card can expose only state; its inner instance follows the inherited mode. Literal properties keep their existing behavior. This syntax binds a direct VARIANT property; it does not search for a similarly named property in arbitrary descendants.
Repeated Apply does not replace a healthy binding. Smart repairs bindings on verified authored targets. Removing an authored token binding restores its recorded previous literal only while the exact owned alias is still present; a manual replacement is preserved. Existing scale, size constraints and manual content retain their separate ownership rules.
To clear a stale mode override from an imported collection, address the collection explicitly:
{
"variableModes": {
"Breakpoints": {
"type": "library",
"key": "<collection-key>",
"library": "<library-name>",
"mode": "auto"
}
}
}
key is the collection key, not a variable key. auto clears only this layer's explicit mode and restores inheritance; it does not reset the parent. Apply this style to each layer that has an unwanted override. library is an optional ownership assertion and requires that library to be available. A same-name local collection is never substituted. Existing string selectors such as "Themes": "Dark" continue to address local collections.
Migrating a variant matrix
Apply updates the existing ComponentSet in place when the number of variants or the set of axes changes: the set and the retained main components are not recreated, so their IDs and instance links survive. The mapping from existing to new variants is built automatically: exact keys first, then variants that agree on the values of the shared axes (an axis removed, added, or both), and when several candidates fit one new variant, a deterministic choice: the set's default variant, then set order. The chosen map is reported in the result warnings. New keys are built by the regular path, adding values on unchanged axes uses the regular add-variants path, and renaming axes or values with an unchanged count uses the existing rename path. Structure, property and style changes are applied in the same Apply.
Document instances of retired variants are moved to the surviving variant with the same shared-axis values by a native component swap — before any main component changes, while the set still has its previous axes. Only the instance's real overrides are captured before the swap: by the native override metadata and the difference from the declared default (component properties: text, booleans, instance swaps, variable bindings) and by direct overrides of nested layers (layer text, a swapped nested instance, visibility) addressed by layer path. Inherited defaults are not overrides and follow the new JSON after Apply. After the swap every real override is read back: unchanged means preserved, a value the native swap changed is written back and verified. A removed axis, property or layer that the surviving variant no longer has is an expected loss per JSON and is listed in the warnings. A compatible override that could not be kept or verified stops the migration before the source variant is removed: the result is unsuccessful, tracking is unchanged and nothing is deleted. This check runs twice: right after the swap and again after the main components were updated, before the source variants are removed, against the saved snapshot — a cached result is never used. Slot content follows Figma's swap heuristics and is not verified by readback. When an axis value is removed (for example tablet), its instances have no compatible variant: Apply stops before any write and suggests the nearest surviving component for the explicit consumers field, or swap those instances manually. Published variants are removed only with the explicit map below: the Plugin API cannot see their instances in other files; those instances keep rendering but lose the library link once the removal is published. When no axis is shared, the explicit map is required: the error carries a paste-ready positional proposal that can be edited; a retained component must not contradict its destination key on a shared axis.
An explicit map at the composition's top level overrides the automatic choice:
{
"variantMigration": {
"rootId": "<existing-component-set-id>",
"retained": {
"state=default": "<retained-default-component-id>",
"state=hover": "<retained-hover-component-id>"
},
"retiredIds": ["<unused-component-id>"],
"consumers": { "<unused-component-id>": "<retained-default-component-id>" }
}
}
Include in retained every destination variant that has a compatible existing component; a key with no compatible component may be omitted and is created; a repeated Apply with the same map keeps the created component by its exact key. List every removed component in retiredIds; consumers is optional and names the retained component that receives the instances of a retired variant without a compatible destination. A retained component must not contradict its destination key on a shared axis; across a full axis rename the map is the author's decision. The same completed map may be repeated. Generate cannot use a migration for a missing root.
Collisions, consumers without a destination, unreadable publication status and a failed component swap stop the operation; nothing changes before the first write. Retained set, component and child identities are preserved. If one composition consumes variants being removed from another, migrate the consumer first.
Do not edit or publish concurrently with migration. Each removal rechecks publication and consumers immediately beforehand. A late change or write failure stops the operation, reports remaining IDs and leaves no successful completion stamp; Figma does not provide an atomic rollback for already completed writes.