Spec reference
States & state previews
Interaction states, declared once — real pseudo-classes in code, generator-owned preview variants on the canvas, and token overrides down to individual parts.
Declared statesGeneratedCurated#
The interaction states the component must support. Declared once; rendered per surface at that surface’s fidelity — code gets real CSS pseudo-class rules, the canvas gets opt-in preview variants.
states: Array<"hover" | "active" | "focus-visible" | "disabled"> // default: []{
"states": [
"hover",
"focus-visible",
"disabled"
],
"anatomy": {
"root": {
"states": {
"hover": {
"background-color": "{color.action.{variant}.background-hover}"
},
"focus-visible": {
"outline-color": "{color.border.focus}",
"outline-width": "{border.width.focus}"
},
"disabled": {
"opacity": "{opacity.disabled}"
}
}
}
}
}State token overrides — root and partsGeneratedCurated#
part.states maps a state to token overrides: Record<state, Record<cssProperty, TokenRef>>. On the root: the full state vocabulary (background-color, outline-*, opacity, …). On a non-ref part (text, icon, box — never a component ref or slot): color-kind channels only (color, background-color, border-color), rendered as descendant rules under the root’s state selector (.root:disabled .label { color: … }) and applied inside canvas state-preview variants.
Refusals: Each of these fails the build by name — the generator refuses, it never papers over (source: core/emit-react.ts validateContract, exercised by the C2 eval family).
- unknown state names; a part override for a state the contract’s
statesdoes not declare - states on a component-instance part (the child contract owns its styling) or on a slot (the consumer owns its content)
- non-color channels on non-root parts
{
"element": "span",
"tokens": {
"color": "{color.text.primary}"
},
"states": {
"disabled": {
"color": "{color.text.secondary}"
}
}
}No shipping contract in contracts/ uses part-level state overrides yet — the capability came from a brownfield field case (a disabled table row washing out its label) and is exercised by npm run extract:figma:partstate:check against committed fixtures. The example above is validated against the live schema at build time.
States by propGeneratedCurated#
statesByProp (v17, the hover-plane round) — an interaction state whose binding is also a function of an enum axis. states holds one ref per channel, and a root ref carrying a single {prop} placeholder already expands into per-value state rules — but that reaches only a token family ({button.bg.hover.{variant}}). The far commoner case is a design system whose per-variant state colours are unrelated names: Eventz’s hover backgrounds are comp/button/primary/color/background/hover on primary and comp/button/color/background/knockout-hover on knockout — no family, no shared stem, nothing a placeholder can reach. Measured, refusing that shape cost Button and Icon Button their entire hover and active planes, and a state crossed with a variant axis is the single most common pattern in a real design system.
The shape is tokensByProp’s plus the state it applies to — one map grammar in the vocabulary, not two. Entries are ordered like tokensByProp entries; refs are plain (the axis is already pinned by the map key). Code renders enum-class state rules (.variant-primary:hover), emitted after the plain states rules so the per-value binding wins at equal specificity — the tokensByProp cascade discipline applied to a state selector.
The driving enum prop, by canonical name.
The declared interaction state the map applies to.
enum value → (CSS property → plain TokenRef).
Refusals — the same discipline states is held to: Each of these fails the build by name — the generator refuses, it never papers over (source: core/emit-react.ts validateContract, exercised by the C2 eval family).
- unknown state names; a state the contract’s
statesdoes not declare - component-instance and slot parts; non-color channels on non-root parts
- the driving prop must be a declared enum; every map key one of its canonical values
- a channel bound for the same state by BOTH
statesandstatesByProp— ambiguous, refused by name rather than resolved by sheet order
{
"statesByProp": [
{
"prop": "variant",
"state": "hover",
"map": {
"primary": {
"background-color": "{comp.button.primary.color.background.hover}"
},
"knockout": {
"background-color": "{comp.button.color.background.knockout-hover}"
}
}
}
]
}Canvas state previews (figmaStatePreviews)GeneratedCurated#
Code gets real :hover/:focus-visible/:disabled; the canvas cannot run pseudo-classes, so real systems hand-build “State=Hover” variant axes — and those rot. figmaStatePreviews: true makes the generator own that axis instead: a State variant axis (Default, Hover, …) where each non-default state applies the state’s token overrides on top of the variant’s base bindings. The mirror image of code-only events: state previews are canvas-only, and the code surface is completely unaffected.
Bounded explosion: previews multiply only the primary enum axis — the one the overrides substitute ({color.action.{variant}.background-hover} names variant); every other axis sits at its default.
The axis is live, not just drawn. The generator wires Figma prototype reactions from every State=Default cell on the default plane to its twin: ON_HOVER → CHANGE_TO State=Hover, ON_PRESS → CHANGE_TO State=Active, both auto-reverting, always with transition: null (durations are not contract facts). focus-visible and disabled are excluded by name: Figma’s trigger vocabulary has no focus and no disabled trigger, so those cells stay static previews — destinations of nothing. Cells whose non-primary axes sit off their defaults have no twin, so they carry no reaction: a named coverage limit that follows from the bounded explosion above.
The opt-in is refused by name when: Each of these fails the build by name — the generator refuses, it never papers over (source: core/emit-react.ts validateContract, exercised by the C2 eval family).
- the contract declares no states
- any declared state has no root token overrides — its preview would render identically to Default
- overrides substitute more than one enum prop
- a prop already binds the design property
State
The differ treats the axis as contract API in both directions: a missing axis on an opted-in contract is figma BEHIND; a hand-built State axis without the opt-in is figma AHEAD — the kit-rot detector — and the proposed patch is the honest one: figmaStatePreviews: true, never a bogus state prop.
{
"figmaStatePreviews": true,
"states": [
"hover",
"focus-visible",
"disabled"
]
}