Skip to content

States

processing.states classifies your library’s Figma variant props as semantic states, enabling two downstream behaviors:

  • The css transformer emits real CSS pseudo-classes and ARIA attribute selectors instead of data-* attributes for classified props.
  • The contract transformer omits browser-driven props from generated Props interfaces.

Declare the classification once and both transforms apply it consistently.

How it works

The same config drives both CSS and contract output — classify a prop once, and both transforms apply it consistently.

Props from Figma

These props are typical outputs from specs generate — the raw Figma variant structure before any state classification is applied:

props:
state:
type: string
default: rest
enum:
- rest
- hover
- pressed # cross-platform name; maps to :active on web
isDisabled: # library uses "is" prefix convention
type: boolean
default: false
focused:
type: boolean
default: false

processing.states acts on these props during specs transform — to determine CSS selector strategy and contract inclusion. The api.yaml itself is not modified.

State concepts

Each concept resolves to a canonical CSS selector and determines whether the prop is included in or omitted from the generated Props interface.

ConceptCSS selector(s)Contract
hover:hoveromitted
active:activeomitted
focus:focus-visibleomitted
focus-visible:focus-visibleomitted
focus-within:focus-withinomitted
placeholder-shown:placeholder-shownomitted
disabled:disabled, [aria-disabled="true"]included
readonly[readonly], [aria-readonly="true"]included
required[required], [aria-required="true"]included
invalid[aria-invalid="true"]included
valid[aria-invalid="false"]included
selected[aria-selected="true"]included
checked:checked, [aria-checked="true"]included
indeterminate:indeterminate, [aria-checked="mixed"]included
expanded[aria-expanded="true"]included
collapsed[aria-expanded="false"]included
pressed[aria-pressed="true"]included
busy[aria-busy="true"]included
current[aria-current="true"]included

Mapping Props to Concepts

Declare mappings under processing.states in your specs configuration. Use prop to name the Figma variant prop and value for the specific enum value that activates the concept.

Partial specs.config.yaml
config:
processing:
states:
active:
prop: state
# Figma value "pressed" → active concept → :active on web
value: pressed
disabled:
# "is" prefix convention → disabled concept → :disabled / aria-disabled
prop: isDisabled

Figma naming conventions don’t need to match the concept name. Many design systems name their pointer-down state pressed rather than active because pressed is platform-neutral — it maps to :active on web, UIControlState.highlighted on iOS, and press Indication in Compose. Naming it active in Figma would embed a web-specific term into a shared design language. Similarly, a library using isDisabled as its boolean prop convention is still expressing the disabled concept.

:::tip Setting up for the first time? Run the CSS States Setup skill in Claude Code — it scans your specs output directory, matches variant props against the concept table, and proposes a ready-to-paste processing.states block. :::

CSS transform

Without states config, all variant props produce data-* attribute selectors:

/* Without states config */
.ds-text-input[data-state="hover"] { … }
.ds-text-input[data-is-disabled] { … } /* boolean prop → presence selector */
.ds-text-input[data-focused] { … } /* boolean prop → presence selector */

With states config, classified props produce semantic selectors:

/* With states config — disabled concept configured */
.ds-text-input:hover:not(:disabled):not([aria-disabled="true"]) { … }
.ds-text-input:disabled,
.ds-text-input[aria-disabled="true"] { … }
.ds-text-input:focus-within { … }

Props not listed in states continue to emit as data-* attribute selectors. Unmatched values on a classified prop (e.g. the rest default on a state prop) are treated as the base state and skipped — the base block already covers them.

Contract transform

Browser-driven concepts (hover, active, focus, focus-within, etc.) are omitted from generated Props interfaces — the browser fires these without the application setting anything. Consumer-controlled concepts (disabled, readOnly, validation, etc.) are included — the consumer sets them and the component bridges them to the appropriate HTML or ARIA attribute.

// state and focused omitted — browser-driven, never set by consumers
interface TextInputProps {
disabled?: boolean; // bridges to :disabled / aria-disabled
readOnly?: boolean; // bridges to [readonly] / aria-readonly
validation?: 'none' | 'invalid'; // bridges to aria-invalid
}

Configuration

config:
processing:
states:
# Concept key → { prop, value?, contract? }
# value: the Figma variant value that activates this concept (defaults to "true" for booleans)
# contract: rarely needed — derived from the concept
hover:
prop: state
value: hover
active:
prop: state
value: pressed # Figma uses cross-platform name "pressed"; concept maps to :active
focus-within:
prop: focused # boolean prop; value defaults to "true"
disabled:
prop: isDisabled # library uses "is" prefix convention
readonly:
prop: readOnly
invalid:
prop: validation
value: invalid # only one enum value maps to this concept
expanded:
prop: expanded

Properties

PropertyTypeRequiredDefaultDescription
propstringYesFigma variant prop name (e.g. state, disabled, readOnly)
valuestringNo"true"Figma enum value that activates this concept. Omit for boolean props.
contract"omit" | "keep"Noconcept defaultOverride the concept’s default contract behavior. Rarely needed.

Run specs transform css to regenerate stylesheets after updating this config. Absence of processing.states is safe — all variant props continue to emit as data-* selectors.

Path

config.processing.states

See Also