Skip to content

Appendix — Style mapping reference

What each spec style key becomes in the emitted stylesheet. An appendix to Styles: that page explains the sheet’s shape, this one is the per-key table.

Read it when the CSS is not what you expected and you want to know whether the mapping or the spec is responsible.

Element class naming

Each key in default.elements becomes a CSS class. Component and element keys convert from camelCase to kebab-case.

Element keyCSS selector
root.{component}
any other.{component}__{element-in-kebab}

dsButton → .ds-button; its startVisualAndLabel element → .ds-button__start-visual-and-label.

For the Web Components build the root is the custom element itself, so root rules target :host and qualifiers go inside the functional form: :host([data-size="l"]:hover). Element rules are identical in both builds.

Variant selectors

(sheet/variantSelectors.ts)

A variant’s configuration becomes qualifiers on the root selector. Which kind of qualifier depends on whether the states convention classifies the prop.

ConfigurationEmitsWhy
A prop the states convention classifiesthe concept’s real CSS selector — :hover, :disabled, [aria-selected="true"]the state is carried by the platform or by ARIA, not by a data attribute
Any other enum prop[data-prop="value"]
Any other prop, true[data-prop]scaffolds emit booleans as presence
Any other prop, false:not([data-prop])the attribute is absent when false, so [data-prop="false"] would match nothing
configuration: { appearance: outline, state: hover }
→ .ds-button[data-appearance="outline"]:hover

Four behaviours follow from that split, and each one is a case where the obvious output would be wrong:

A concept with several selectors emits several rules. disabled is :disabled, [aria-disabled="true"] — either may carry the state — so the variant produces one rule per part, and two classified props produce the cartesian product of their parts.

A classified boolean set to false negates its concept. It has no concept of its own, so without this the variant would be read as the resting state and emit nothing — an unselected row, and every hover pairing with it, would have no rule. Negating a multi-part concept is an AND of nots (:not(:checked):not([aria-checked="true"])), never a cartesian expansion.

:hover and :active are guarded against firing while disabled. A disabled control still receives pointer events, so the hover rule would otherwise paint over the disabled one. Applied whenever the convention names a disabled concept.

A concept a nested element’s role announces is routed to the data attribute instead. The root no longer carries the ARIA state — the nested control does — so the root’s rule keys off the variant prop. A prop keeps its classification when another concept still uses it.

A classified value that no concept names is skipped with a warning, naming the prop and value. Skipping is correct for the prop’s resting value, since the base block already covers it; for anything else the declared styling would be dropped silently, which is what the warning exists to prevent.

Variants emit in schema order — variants.yaml puts single-prop variants before compound ones, which is the cascade the variant-layering algorithm assumes. Child element overrides use a descendant selector: .ds-button[data-appearance="outline"] .ds-button__label.

Token references

(values/tokens.ts)

A reference ({ $token: "Color/Primary", $type: "color" }) becomes a custom property derived from the token path:

Color/Primary → var(--color-primary)
Constants/Spacing/5x → var(--constants-spacing-5x)
Typography/font__300__medium → var(--typography-font--300--medium)

Resolution is format-aware. FIGMA_SYNTAX_WEB uses a ---prefixed string as the variable name directly; CUSTOM prefers an explicit $cssVar; every other format derives the name from the path. See Token format.

Characters invalid in a CSS dashed-ident — parentheses, %, & — are dropped, and each drop is counted in a warning summary at the end of the run.

A variable Figma could not resolve at capture time is withheld, not emitted. Its sentinel name derives a perfectly ordinary custom property that nothing defines, so the reference would be dead on arrival — and worse than nothing where it lands on a size, which collapses the element to zero. The ladder is: the captured raw value the spec carries, then unset for a single-property declaration, then no declaration at all. A value composed into a shorthand keeps returning nothing, so the composite falls back to its own zero rather than embedding a keyword where a length belongs.

Fills

(style/fills.ts)

Spec keyCSSNotes
backgroundColorbackgroundnull → transparent
textColorcolornull → transparent
fillColorbackground-color, fill, or backgrounddepends on the element’s anatomy type — see below

fillColor’s target is decided by the element type and nothing else:

  • glyph / vector → background-color, which shows through the mask the element paints with
  • untyped → fill, the legacy SVG paint
  • anything else → background

Colour values resolve as: token reference → var(--…), hex string → as-is, colour object → its hex, else currentColor.

A gradient maps to a CSS gradient function, with stop colours run through the same rules so token stops become var(--…):

GradientCSS
LINEARlinear-gradient(Ndeg, …) — the spec angle is already CSS convention
RADIALradial-gradient(at X% Y%, …)
ANGULARconic-gradient(from 0deg at X% Y%, …) — both start at 12 o’clock

An angular gradient carries no angle (ADR-003); the rotation it appears at is the node’s, applied as a transform. A gradient textColor paints as a background clipped to the glyphs (background-clip: text; color: transparent). A gradient fillColor is skipped for untyped elements, which have no inline-gradient equivalent.

Opacity

(style/opacity.ts)

Spec keyCSSNotes
opacityopacitya plain number passes through; a token reference becomes calc(var(--x) * 1%)

Opacity variables are authored on a percentage scale (36, not 0.36) because that is what reads naturally across platforms. Emitting the variable bare would give opacity: 36, which clamps to 1 and silently discards the state.

Border

(style/border.ts)

Every stroke alignment emits an outline, not a border. A Figma stroke costs no layout space at any alignment, and a CSS border does for a hugging element — a badge came out 2px taller than the design for exactly that reason. An outline affects no layout and follows border-radius.

Spec keyCSSNotes
strokesoutline-color + outline-style: solidnull clears both mechanisms (border-color: transparent, outline-style: none), since a variant dropping its stroke must cancel what the default drew
strokes (gradient)nothing on the elementpainted as a ::before ring instead — see below
strokeWeightoutline-widthplus a negative outline-offset for inside strokes. A Sides object keeps the border mapping, as an outline has one width. null zeroes both
strokeAlignoutline-offsetINSIDE (and an unrecorded alignment, Figma’s default) is pulled back over its own edge; OUTSIDE and CENTER sit where the outline falls

A gradient stroke paints as a ::before ring, and the element itself declares no stroke at all. No outline can take a gradient; border-image is the only border property that can and it ignores border-radius, so a rounded element would come out square; and painting into the element’s own background destroys whatever fill it declares. The ring covers the host, inherits its radius, and masks out its middle. Its thickness is the pseudo-element’s padding.

An element that mixes gradient and solid strokes across variants emits border-image: none alongside the solid stroke, so the later declaration wins.

A stroke paint with no weight emits border-width: 0 in the element’s default block. Figma reports strokeWeight: 0 for these and the spec carries the paint without the zero, so border-style alone would fall back to CSS’s initial medium — about 3px of border the design does not have.

Corner radius

(style/cornerRadius.ts)

Spec keyCSSNotes
cornerRadiusborder-radiusscalar or token → one value; a Corners object (topStart topEnd bottomEnd bottomStart) → four-value shorthand

Effects

(style/effects.ts)

Spec keyCSSNotes
effects (token reference)box-shadow / filter / backdrop-filternames up to three role variables — <name>-shadows, <name>-layer-blur, <name>-background-blur — each holding its property’s complete value. All three emit with a none fallback, so only the roles the consumer defines take effect
effects.shadowsbox-shadowcomma-joined [inset] x y blur spread color; visible: false entries dropped; an all-invisible list emits box-shadow: none
effects.layerBlurfilter: blur()visible: false → filter: none
effects.backgroundBlurbackdrop-filter: blur()visible: false → backdrop-filter: none
effects: nullall three reset to nonea variant removing the lower layer’s effects

Where one effects value fans out into several properties, each declaration carries an /* effects */ trace comment tying it back to the single spec value.

Size

(style/size.ts)

Spec keyCSS
width, heightwidth, height
minWidth, minHeightmin-width, min-height
maxWidth, maxHeightmax-width, max-height

Zero values are omitted. height is skipped entirely when the element states an aspectRatio — the two are redundant, and only the ratio survives composition: a fixed height beats aspect-ratio in the cascade, so a component placed in a narrower box would keep its full authored height. A style set with aspectRatio: null is stating it has no ratio, so its height is emitted.

Padding

(style/padding.ts, values/sides.ts)

Spec keyCSSNotes
paddingpaddingscalar or token → one value; null → padding: 0
padding (Sides)logical per-side propertiespadding-block-start, padding-inline-end, padding-block-end, padding-inline-start

When all four sides are known they collapse to the two-value logical pair — padding-block and padding-inline — not to a four-value padding, which reads as top/right/bottom/left and would put start on the left in every writing direction.

The same table drives border-width, where the side splits the property name (border-inline-start-width) rather than suffixing it. A property with no entry falls back to physical names.

Typography

(style/typography.ts)

Spec keyCSSNotes
typography (token reference)fontplus letter-spacing: var(--<name>-letter-spacing, normal) — the font shorthand cannot carry tracking and resets it, so the companion variable is referenced explicitly
typography.fontSizefont-sizepx when a number
typography.fontFamilyfont-familyquoted when it contains a space
typography.fontStylefont-weight + font-styleFigma combines weight and slant in one name (“SemiBold Italic”), split here
typography.lineHeightline-heighta number gets px — unitless line-height in CSS is a multiplier; a string passes through
typography.letterSpacingletter-spacingpx when a number
typography.textCasetext-transformUPPER→uppercase, LOWER→lowercase, TITLE→capitalize, ORIGINAL→none
typography.textDecorationtext-decorationUNDERLINE→underline, STRIKETHROUGH→line-through, NONE→none

A sub-property is a literal value unless it is an explicit token reference — a plain string like "Inter" or "120%" is data, not a token name.

Text alignment

(style/textAlign.ts)

Spec keyCSSNotes
textAlignHorizontaltext-alignSTART→start, CENTER→center, END→end, JUSTIFY→justify

Logical directions only (ADR-064). The transform speaks the current schema; Figma’s LEFT/RIGHT/JUSTIFIED are remapped before they reach it.

Text truncation

(style/truncation.ts)

Figma truncates by line count, optionally with an ellipsis. CSS has two mechanisms and they do not overlap.

SpecCSS
maxLines: 1white-space: nowrap; overflow: hidden (+ text-overflow: ellipsis when textOverflow: ELLIPSIS)
maxLines: n > 1display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: n; line-clamp: n; overflow: hidden
textOverflow: ELLIPSIS with no line counttreated as single-line truncation

Both set overflow: hidden, which clipsContent may have stated already — a duplicate is harmless, and omitting it would leave the truncation inert.

Aspect ratio

(style/aspectRatio.ts)

Spec keyCSSNotes
aspectRatioaspect-ratio: x / ynull emits nothing, and means the element’s height is authored — see Size

Visibility and overflow

(style/visibility.ts, style/overflow.ts)

SpecCSS
visible: falsedisplay: none — only when explicitly false
clipsContent: trueoverflow: hidden
clipsContent: falseoverflow: visible

Transform

(style/transform.ts)

Spec keyCSSNotes
rotationtransform: rotate(…)zero omitted — a transform creates a containing block for absolute descendants, so emitting a no-op one changes layout

Raw CSS

(style/rawCss.ts)

_rawCss holds declarations a rule pre-pass injected into the structured data before any mapping ran. Emitted verbatim, and last among the style keys, so a rule can override what the mapping derived.

Position and offsets

(style/position.ts)

Spec keyCSSNotes
position: ABSOLUTEposition: absolute
position: AUTOposition: staticthe element re-enters auto-layout flow; static is what undoes a prior absolute rule
topinset-block-start
bottominset-block-end
startinset-inline-startleft in LTR
endinset-inline-endright in LTR

With no explicit position, coordinates can still imply absolute placement. Outside auto-layout any coordinates do. Inside it, children are flow-placed, so coordinates only count when they are meaningful — non-zero, or anchored on opposing sides (top+bottom, start+end). Zero-only single anchors are canvas noise.

Layout

(style/layoutToCSS.ts)

Spec keyCSSNotes
layoutMode: HORIZONTALdisplay: flex; flex-direction: rowsuppressed when visible: false, which emits display: none in the same rule
layoutMode: VERTICALdisplay: flex; flex-direction: columnas above
layoutMode: NONE or nulldisplay: block
mainAxisAlignmentjustify-contentSTART→flex-start, END→flex-end, CENTER→center, SPACE_BETWEEN→space-between
crossAxisAlignmentalign-itemsSTART→flex-start, END→flex-end, CENTER→center, STRETCH→stretch, BASELINE→baseline
wrapflex-wraptrue→wrap, false→nowrap
wrapAlignmentalign-contentSTART→flex-start, SPACE_BETWEEN→space-between. Emitted whenever present
itemSpacinggapnumber → px; token → var(--…); { horizontal, vertical } → gap: <vertical> <horizontal>, or a single row-gap/column-gap when only one side is known

A negative itemSpacing emits no gap. Figma expresses overlapping children that way and gap cannot be negative; the overlap becomes a negative margin on every child after the first (see Additional rules).

Sizing within the parent

FILL depends on the parent’s flex direction, which the mapping is told:

SpecParentCSS
layoutSizingHorizontal: FILLHORIZONTALflex: 1 0 0
layoutSizingHorizontal: FILLanything elsewidth: 100%
layoutSizingVertical: FILLVERTICALflex: 1 0 0
layoutSizingVertical: FILLanything elseheight: 100%
layoutSizingHorizontal: HUG—width: fit-content
layoutSizingVertical: HUG—height: fit-content
FIXED—nothing; the explicit width/height carries it

Cross-axis FILL is deliberately not align-self: stretch. Stretch means “ignore the parent’s alignment, start at the edge and fill”, so the moment a min/max constraint stops the child actually filling, the leftover space lands on one side and the parent’s alignment has already been discarded — a dialog’s max-width’d body sat against its padding edge where the design centres it. The two are identical while the child does fill, which is most of the time and why it went unseen.

Additional rules

Some spec facts produce whole rules rather than declarations on the element.

Spec factRuleModule
A negative itemSpacing> * + * with a negative margin on the parent’s main axissheet/elementRules.ts
A gradient strokesa ::before ring, masked to its paddingsheet/elementRules.ts
An instance element with a stated size> * at width/height: 100%, so the composed child fills the slot rather than painting at its own master’s sizesheet/elementRules.ts
A slot element:empty and [data-empty] → display: none, so an unfilled slot stops taking the parent’s gapsheet/defaultBlock.ts
An element whose role emits a native controla reset of the user-agent styling that tag bringssheet/roleResets.ts
A checkbox or switch rolethe hidden input, the click-target label, and a focus ring re-drawn on the visible proxysheet/roleResets.ts
The states convention naming an active or pressed conceptcursor: pointersheet/cursors.ts
The states convention naming a disabled conceptcursor: not-allowedsheet/cursors.ts

Everything generated sits in one @layer specs cascade layer, so an unlayered consumer rule beats it regardless of specificity. The root and its descendants also get box-sizing: border-box: a Figma frame’s width includes its padding and CSS’s default excludes it, so without this every element with both a fixed dimension and padding renders larger than the spec by exactly its padding.

Skipped keys

Spec keyReason
lockedFigma layer lock — no CSS equivalent
cornerSmoothingFigma’s squircle algorithm — no CSS equivalent
primaryAxisSizingModeFigma internal — no CSS equivalent
textAlignVerticalthe parent’s align-items carries it in most cases
centerHorizontalOffset, centerVerticalOffsetno CSS equivalent for CENTER-constrained Figma positioning
DIAMOND gradientsexcluded at the schema level — no native equivalent on any target platform

See Also

  • Styles — the sheet’s shape, and what a role adds to it
  • Token format — which naming format the references use
  • States — the convention that decides which props become real CSS selectors