Facets
Style in Milda is a small, abstract paint vocabulary, the facets, each bound to a token and keyed by states a behaviour emits and conditions over props. There are no CSS properties in the model and no per-platform style code anywhere: divergence lives entirely in how tokens resolve.
#What a facet is
A facet is a slot on a part: a what to paint, never a how. “This part's fill is the surface token” is a statement every platform can honour; background-color is not. On the surface a facet is a word followed by its values: a dotted value is a token reference, a number is a literal, a bare word is an intent keyword.
part Button: container { fill accent ink on-accent border line hairline solid corner radius.sm inset space.sm space.lg text body weight weight.medium elevation shadow.card opacity 90 }
#The vocabulary
The prelude defines 25 facets. For each: what it covers, and where the targets diverge in realizing it.
| Facet | Covers | Divergence |
|---|---|---|
fill | surface/background | - |
ink | text + icon foreground | - |
border | edge: color + width (hairline|thin|thick) + style | iOS often overlay-stroke |
border.top | top edge (physical side) | - |
border.right | right edge (physical side) | - |
border.bottom | bottom edge (physical side) | - |
border.left | left edge (physical side) | - |
border.inline-start | leading edge (logical, writing-direction) | - |
border.inline-end | trailing edge (logical, writing-direction) | - |
border.block-start | block-start edge (logical) | - |
border.block-end | block-end edge (logical) | - |
corner | corner radius (token role) | - |
inset | internal padding (block/inline) | - |
gap | spacing between children | - |
text | typography role (body|caption|title…) - bundles size/weight/line/tracking | iOS Dynamic Type = OS-resolved role |
textClamp | truncate to N lines + ellipsis | web -webkit-line-clamp · .lineLimit · maxLines+overflow |
textWrap | wrap | nowrap | break | - |
weight | font-weight (typography axis §3, split out of the `text` role bundle) | web font-weight · .fontWeight · TextStyle.fontWeight |
font | font-family (typography axis §3) | web font-family · UIFont · Typeface |
slant | font-style: normal | italic | oblique (typography axis §3) | - |
elevation | depth | web box-shadow · Android tonal-elevation · iOS none/sheet |
overlay | state layer (Material hover/press tint) | Material-specific; web ≈ bg shift |
opacity | transparency | - |
ring | focus indicator | desktop ring · mobile none · tv strong highlight |
backdrop | scrim / blur behind layer | iOS material/vibrancy |
- A facet name may carry one sub-key:
text.clamp(truncate to N lines),text.wrap,border.inline-start(one logical edge) orborder.top(one physical edge). bordertakes up to three values in any order: a colour token, a width token (aborder-typed token such as a hairline), andsolid|dashed|dotted.insettakes one or two tokens: all sides, or block then inline. One axis isinset.inlineorinset.block, and one edge isinset.inline-start,inset.inline-end,inset.block-startorinset.block-end. The edges are logical, so the start edge is the leading side in every writing direction, and the most specific statement wins on its edge.texttakes a typography token or a font-size token;font,weightandslantoverride one axis of the role.
#Facets bind to tokens
A facet's value is a token reference. Tokens are typed and layered, and a token can answer per context; binding a facet to a token rather than a literal is what makes a restyle a foundations change and a dark scheme one more alias. A facet cannot be context-aliased itself (fill alias by … is an error): point it at a token and put the alias on the token. The mechanism, including platform divergence through elevation, ring and backdrop tokens that alias by platform, is on Tokens & context.
A facet value that resolves as a token takes the token; a bare number falls back to a literal only when no token claims it. So opacity 50 and text.weight 600 work as literals, while a fill with no token behind it is reported, not guessed.
#States and conditions
Paint changes under two kinds of condition, and the language has one word for each. is a behaviour state: hovered, pressed, focus-visible, selected, open, checked, and the rest of the vocabulary the atoms emit, plus the ambient gates (disabled, readonly) and collection positions (first, last, odd, even). The list is closed: any other word is reported, and state hover is told it meant hovered. is a predicate with no state involved: a prop value, a context, an ancestor's state. The two combine.
part option: item { fill surface state active { fill surface.raised } state selected { ink accent } state disabled { opacity 50 } }
part Button: container { fill accent when variant = Secondary { fill surface } state hovered when variant = Secondary { fill accent-subtle } when context ColorScheme = Dark and state focused { ring on-accent } } part icon: icon { ink text when ancestor Button hovered { ink accent } }
- Predicates combine with
and,or,notand parentheses, compare with=,!=,<,contains, and take four kinds of leaf: a prop, a , acontext, and anancestor <part> <state>, which styles a descendant on an ancestor's state. - A state can come from a prop:
bind state disabled disabledputs the part into the state while the prop is true. - Layout can change too: a or block may carry a
layout { }block beside its facets.
#Three layers
| The spec defines | A .milda file can say | The React target does |
|---|---|---|
| The facet vocabulary above, with one sub-key per facet, token-valued, keyed by behaviour states; divergent facets as abstract token types aliased by platform. | Every facet in the table, with the value forms above; and blocks with the full predicate grammar; a block of transitions. | Maps each facet to CSS declarations whose values are the theme's custom properties; a prop condition to a data attribute and selector; a behaviour state to the pseudo-class or data attribute its realization uses; an ancestor predicate to a descendant selector. The Figma target maps the same facets to Variable bindings; the facet semantics are stated once and each target's coverage of every slot is total by construction. |
#Motion
A motion { transition fill ink duration fast easing standard } block names the facets that animate between states and the tokens that time them: a duration and an easing (Values & literals). Motion adds no state; the behaviour states are its anchors.
Structure and spacing intents live one layer over: Layout.
These words, in the reference
- A paint facet: an abstract slot such as "background" or "text colour", pointed at a token.
- Paint for an interaction state.
- Paint that applies while a condition holds, with no interaction state involved: the variants of a component.
- How the part animates between its states.
- A condition.
Open a word for its entry, or read them all in the reference.
See it built in the tour4. Variants and states10. Type, motion and computed values