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.
Updates
Release history for the Composition type. The newest release is on top; open an entry to see what changed.
2.7.7Latest
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. With multi-select, output may contain $synthetic: true to indicate a synthetic set construction (informational only).
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.
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.
| Mode | Best for | Result |
|---|---|---|
Generate | New component / component set | Creates new output |
Apply | Existing tracked component updates | Updates existing nodes with tracking |
Automatic mode picks the operation from context (selected anchor, tracking, local component presence).
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.
- In Automatic mode, a stale or cross-file identity with no live candidate in the current Figma document creates new output.
- 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 |
$metadata | any | — | Opaque tooling metadata; ignored by generation |
size / style | object | — | Optional embedded token blocks (see Advanced fields) |
slotHostPipeline | object | — | Advanced host/slot systems (see Advanced fields) |
segmentIconPostPassRefMarkers | string[] | — | Advanced icon-sync markers |
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.
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). Never both.
{
"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 | Library hint | "library": "SXL DS" |
key | Publish-key hint | "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" |
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}"
}
}
}
ref.nested — target nested instances by layer name:
"nested": {
"Badge": {
"properties": { "label": "3" },
"icon": { "component": "fire-3", "properties": { "style": "filled" } }
}
}
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 | Required |
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.
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.
"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 | "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) | "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" }
Layer size
| Property | What it does — values | Example |
|---|---|---|
widthType / heightType | Sizing mode — fixed, hug, fill | "widthType": "fill" |
width / height | Fixed size (px) | "width": 360 |
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.
| Property | What it does | Example |
|---|---|---|
rows / columns | Track counts | "columns": 3 |
rowGap / columnGap | Track gaps (px) | "columnGap": 8 |
gridTemplateRows / gridTemplateColumns | Track sizes | "gridTemplateColumns": "1fr 1fr" |
justifySelf / gridAlignSelf | Grid-child align (H / V) — start, center, end, auto | "justifySelf": "center" |
gridRowSpan / gridColumnSpan | Grid-child span (tracks) | "gridColumnSpan": 2 |
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>" | "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 |
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.
"$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.
| 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 |
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": … } |
Shorthand — order is trigger animation duration easing [direction]:
"transitions": { "hover-in": "on-hover smart-animate 200ms ease-out" }
Object form with a condition:
"transitions": {
"hover-in": {
"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}" } }
Slot-host pipeline
For host/item systems (segmented controls, tabs, item groups), slotHostPipeline wires a host set to its item set. slotItemComponentSetName is required. If any composition file defines slotHostPipeline, the legacy WSegmentControl defaults are not applied — declare every host/item set you rely on.
"slotHostPipeline": {
"hostComponentSetName": "WSegmentControl",
"hostCompositionTokenName": "WSegmentControl",
"slotName": "item-group",
"slotItemComponentSetName": "WSegmentControlItem"
},
"segmentIconPostPassRefMarkers": ["WSegmentControl", "WSegmentControlItem"]
Both keys are also accepted under a $figma wrapper ($figma.slotHostPipeline, $figma.segmentIconPostPassRefMarkers).
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": { "hover-in": "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 |
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 → MIN/CENTER/MAX/SPACE_BETWEEN |
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 |
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.