Coming from CSS
If you think in CSS, this page is the dictionary. For each property you would reach for, it gives the word a .milda file uses, the exact spelling, why the word is different, and the corner cases. Where the language has no word, it says so and says what to write instead.
#Four things to know first
Every entry below is a consequence of these, so they are worth thirty seconds.
- There are no CSS properties in the file. A part has - fill, ink, border, corner, inset - and each target maps them to its own primitives. The web target maps them to CSS; Figma maps them to Variable bindings. The name is chosen for what the thing is, never for how one platform draws it.
- Values are tokens. A facet points at a typed token (
space.sm,accent), not at a literal. That is why dark mode is one alias in foundations rather than a second stylesheet, and why there is no#7c3aedanywhere in a component. - States are named, not observed. A pseudo-class is how a browser notices an interaction. The file names the interaction state (
hovered,pressed,checked) and each target observes it its own way. - Layout is propose/choose, not flexbox. A container proposes room, each child says what it wants (
fit,fill, a weight, a fixed token, a ratio), the container places it. Flexbox can express that, and the web generator writes the flexbox for you.
#The lookup
What you would write in CSS, and what you write in Milda. Each row links to its entry.
| In CSS | In Milda | Where |
|---|---|---|
| padding | inset | Facet |
| padding-left, padding-inline-start, padding-inline | inset.inline-start, inset.inline | Facet |
| margin | nothing - gap, bleed, or alignment | Not in the language |
| gap | gap | Layout |
| border, border-width, border-color, border-style | border | Facet |
| border-radius | corner | Facet |
| box-sizing, display: block, overflow | nothing - there is one box | Not in the language |
| background, background-color | fill | Facet |
| color | ink | Facet |
| box-shadow | elevation | Facet |
| opacity | opacity | Facet |
| outline, outline-offset (the focus ring) | ring | Facet |
| font (the shorthand), font-size, line-height | text | Facet |
| font-weight | weight | Facet |
| font-family | font | Facet |
| font-style: italic | slant | Facet |
| text-overflow: ellipsis, -webkit-line-clamp | text.clamp | Read, not yet drawn |
| white-space, text-wrap | text.wrap | Read, not yet drawn |
| text-align | nothing - the container places the text | Not in the language |
| display: flex, flex-direction | layout { stack row | column | layers } | Layout |
| justify-content | distribute | Layout |
| align-items | align | Layout |
| flex-wrap | wrap | Layout |
| width, height | size <width> <height> | Layout |
| flex: 1, flex-grow, flex-shrink, flex-basis | fill, or weight(n) | Layout |
| min-width, max-width, min-height, max-height | min(token) max(token) after an intent | Layout |
| aspect-ratio | ratio(w:h) as an axis intent | Layout |
| display: grid, grid-template-columns | grid - reserved | Read, not yet drawn |
| position: absolute, top/left | position overlay, inside a layers stack | Layout |
| position: sticky, position: fixed | position pinned <edge> | Layout |
| negative margin (margin: 0 -16px) | position bleed <edge> | Layout |
| z-index | nothing - the order of the parts | Not in the language |
| :hover, :active, :focus, :focus-visible, :disabled, :checked | state hovered | pressed | focused | focus-visible | disabled | checked { } | Condition |
| [data-variant="secondary"], .button--secondary (BEM) | when variant = secondary { } | Condition |
| .card:hover .icon (styling a descendant on an ancestor’s state) | when ancestor <part> <state> { } | Condition |
| @media (prefers-color-scheme: dark), .dark, [data-theme] | context-group ColorScheme + alias by ColorScheme on the token | Foundations |
| --space-sm: 8px; var(--space-sm) | token space/sm : spacing = 8px, referenced as space.sm | Foundations |
| px, rem, em, %, vw | px (density-independent) and scaled() | Foundations |
| #7c3aed, rgb(), hsl(), oklch(), color() | hex(), rgb(), srgb(), p3(), oklch(), hsl() | Foundations |
| calc(), color-mix() | compute | Foundations |
| currentColor, inherit | an unstated border colour; an unstated facet | Facet |
| !important, specificity, the cascade | nothing - a part states its own facets | Not in the language |
| transition | motion { transition <facets> duration <token> easing <token> } | Motion |
| @keyframes, animation, transform | nothing yet | Not in the language |
| display: none, visibility: hidden | present when <condition> | Condition |
| cursor, user-select, pointer-events | nothing - the behaviour decides | Not in the language |
| Whatever is not here | a slot, a per-target option, or foreign content | Not in the language |
#The box
The space inside a part, its edge, and its corners. The box model has three layers in CSS; a part has one.
padding
FacetPadding is the name of a layer in the CSS box model. What a designer means by it is a geometry: the content sits in from the part’s edge by some distance. inset names that distance, in the word iOS, Figma and layout engines already use, and says nothing about a box model that only the web has. The value is a spacing token, so a density change is a foundations change.
part Demo.card: container { inset space.md } part Demo.button: container { inset space.sm space.lg }
- One value is all four edges. Two values are block then inline - vertical then horizontal - the same order as the two-value CSS shorthand. There is no three- or four-value form; a single axis or edge has its own statement, see the next entry.
- In the Studio the Style tab has one Inset control with three modes - the same value everywhere, one per axis, one per edge - which write exactly the statements this page describes.
- Inside a
layout { }block,insetis the same facet - either place is fine, not both. - A child that must reach past the parent’s inset (a full-bleed image in a padded card) does not get a negative margin; it says
position bleed, see margin below.
- The React target writes
paddingwhen the four edges agree,padding-block/padding-inlinewhen each axis is symmetric, the four logical longhands otherwise.- Read more
- /language/styling
inset.inline-start, inset.inlineA leading icon wants less room on its own side; a nested row wants its indent on the start edge only. Those are paddings on one edge or one axis, and each is its own statement beside the bare inset. The edges are LOGICAL - start and end along the line, start and end across it - because a design system that says left means the wrong side in Arabic, and one that says inline-start means the leading side in every language.
part Demo.chip: container { inset.block space.xs inset.inline space.md } part Demo.button: container { inset space.sm space.lg inset.inline-start space.md }
- The axes are
inset.blockandinset.inline; the edges areinset.block-start,inset.block-end,inset.inline-startandinset.inline-end. - The more specific statement wins on its edge:
inset space.sm space.lgwithinset.inline-start space.mdis small top and bottom, large at the end, medium at the start. - A hover or a
whenblock overrides exactly the edges it states, the way a CSS rule does:inset space.lgunderstate hoveredreaches all four edges, whatever the base said per edge. inset.leftand the other physical words are read as the logical edge they name left to right, with a warning, and never written back.
- The React target writes
padding-inline-startand the other logical longhands;padding-inline/padding-blockwhen an axis is symmetric.- Read more
- /language/styling
margin
Not in the languagenothing - gap, bleed, or alignmentA margin is a child reaching outside its own box and dictating the parent’s layout. In the propose/choose model the parent owns the space between its children, so that space is the parent’s gap; a child that must extend past the parent’s padding says position bleed; and margin: auto was always an alignment, so it is distribute or align on the parent. Each of the three means the same thing on every platform, which no margin does.
part Demo.card: container { layout { stack column; gap space.md } inset space.lg } part Demo.card.cover: content { layout { position bleed inline block-start } }
- There is no margin collapsing, because there is no margin.
- To push one child to the far end of a row, give the row
distribute between, or give a spacer siblingsize fill.
- The React target writes
gapon the container; a bleed is a negative logical margin computed from the parent’s inset.- Read more
- /language/layout
gap
LayoutThe same word, because CSS got this one right: the space between children belongs to the container. It takes a spacing token.
part Demo.row: container { layout { stack row; gap space.sm; align center } }
- One value: there is no separate row-gap and column-gap.
- It only does something on a part with a
stack; a part without one is not a layout container. gapmay also be written as a facet line outside thelayout { }block. Both land in the same place.
- The React target writes
gap.- Read more
- /language/layout
One facet, up to three values in any order: a colour token, a width token, and solid, dashed or dotted. Which slot a word fills is decided by the type of the token it names, so the order never matters. A width token has the type border, which is a length type like spacing: a hairline is a token, not a number.
part Demo.input: input { border line hairline solid } part Demo.divider: container { border.block-end line hairline } part Demo.tab: container { border.inline-start hairline }
- Any one of the three states a border.
border accentalone is a 1-unit solid edge in that colour;border hairlinealone is a hairline whose colour follows the ink. - An unstated colour follows the ink. On the web that is
currentColor; in Figma it is a binding to the ink variable. It is the same rule everywhere, stated once. - One edge at a time:
border.top.right.bottom.leftare physical,border.inline-start.inline-end.block-start.block-endare logical and survive a right-to-left locale. Per-edge borders are a thing Figma cannot draw, and the Figma target says so rather than guessing. - A word that is not a declared token -
transparent, or a typo - is placed by position: colour first, then width. Soborder transparent hairlinereads as you expect, andborder hairlneis a border with no width.
- The React target writes
border-style,border-widthandborder-color(or the per-side longhands), never thebordershorthand.- Read more
- /language/styling
border-radius
FacetA radius is how CSS draws a rounded corner; the design decision is the corner. The value is a radius token, and pill is the radius that fully rounds the shorter side - a word rather than 9999px, so every target can draw a pill without knowing the box’s size.
token radius/sm : radius = 6px token radius/pill : radius = pill
- One value for all four corners. A single corner, or the elliptical two-radius form, has no spelling.
- A literal such as
50%is passed through to the web as written, and means nothing to a target without a box to take a percentage of. Point at a token.
- The React target writes
border-radius.- Read more
- /language/styling
box-sizing, display: block, overflow
Not in the languagenothing - there is one boxA part has one size, and its inset is inside it; there is no content-box to choose. display is a target decision: a part is a container, an item, a text, and the target picks the element and how it flows. Overflow handling is a realization detail that differs by platform (a scroll view is a view on iOS and a property on the web), and the language does not state it yet.
- Read more
- /thinking
#Colour and depth
Every colour and shadow is a token. A facet never carries a colour literal, which is what makes dark mode one alias rather than a second stylesheet.
fill is what every drawing API calls the paint inside a shape. “Background” implies there is a foreground drawn over it in the same box, which a text part or an icon does not have. The value is a colour token.
part Demo.button: container { fill accent state hovered { fill accent-hover } }
- A facet names a token; it does not take a colour literal.
fill hex(7c3aed)is not a fill, because the literal belongs in a token declaration, where it can be aliased and themed. transparentis the one colour word a facet accepts directly. A document that declares a token by that name wins, as with every bare word.- Background images and gradients are not a fill. An image is a
contentpart with asource; agradienttoken type exists in foundations and reaches the theme, not a facet.
- The React target writes
background, with the token’s custom property as the value.- Read more
- /language/styling
color
Facetcolor in CSS means text colour, inherits down the tree, and is the fallback for borders and SVG through currentColor. ink says the foreground outright: text and icon colour, and the colour a border takes when it states none. The web’s inheritance is one target’s realization of that; the facet does not depend on it.
part Demo.label: text { ink text when ancestor Demo hovered { ink accent } }
- A container has no
inkof its own to offer in the Studio; the text and icon parts inside it do. In the text you may state it on a container and the web will inherit it, but the portable place is the part that draws. - An icon draws in the ink: a glyph has no colour of its own.
- The React target writes
color.- Read more
- /language/styling
box-shadow
FacetA shadow is how the web draws depth. Android draws depth as a tonal step, iOS draws it with a different shadow or not at all. The facet states the intent - this part is raised - and points at an elevation token, which aliases by platform to a shadow token on the web and to whatever the other platforms do. The components never know.
context-group Platform { Web iOS Android } foundations { token shadow/card : shadow = raw "0 4px 12px -2px rgba(15,23,42,0.12)" token tonal/1 : tonal = 1 token elevation/card : elevation = alias by Platform { default -> shadow.card; Android -> tonal.1 } }
- A shadow token is written as a
rawstring, because a shadow’s shape is the web’s. That marks it as partial: a target that cannot read the string has nothing to fall back on, which is exactly why the facet points at an elevation rather than at the shadow. - Inset shadows and multiple layered shadows are what the raw string says they are; the language has no vocabulary for them.
- The React target writes
box-shadow.- Read more
- /language/tokens
opacity
FacetThe same word and the same meaning: how see-through the whole part is, from 0 to 1. It points at an opacity token so that a disabled look is one decision in foundations.
foundations { token opacity/disabled : opacity = 0.5 } component Demo { part Demo: container { state disabled { opacity opacity.disabled } } }
- A bare number is the value itself, as every target reads it:
opacity 0.5. The Studio’s built-in scale names its steps by percentage (50,75), and a file with its own foundations has no such token, soopacity 50there is the literal fifty, which the browser clamps to fully opaque. Declare the token.
- The React target writes
opacity.- Read more
- /language/styling
A focus ring is a thing every platform draws and each draws differently; outline is one way to draw it. The facet is the ring’s colour, and nothing else: the width and the offset are the target’s, so a ring looks native everywhere. It is usually stated under state focused or state focus-visible.
part Demo.button: control { state focus-visible { ring accent } }
- A ring colour that fails to resolve falls back to the ink, so a focused part is never invisible.
- A static target (a Figma frame) has no focus, and its coverage record says so: the ring is reported as not drawn rather than approximated.
- The React target writes
outline: 2px solid <colour>andoutline-offset: 2px.- Read more
- /language/styling
#Text
A text style is one token with several fields, and a part names the role. The axes can be overridden one at a time.
A text role - body, title, caption - is one decision with several consequences, so it is one typography token with family, size, weight and line height inside, and the part says text body. A font-size token works in the same slot when only the size is meant.
foundations { token font/sans : font-family = { family: "Inter"; fallback: sans-serif; source: google; license: "OFL-1.1" } token body : typography = { family: font.sans; size: 15px; weight: 400; line-height: 1.5 } token size/sm : font-size = 13px } component Demo { part Demo: container { } part Demo.label: text { text body } part Demo.hint: text { text size.sm } }
- Line height and letter spacing are fields of the typography token, not facets of a part. A line height is a
ratio(1.5), not a length. textis the facet; it is not a keyword for the part’s content. The visible string iscontent "…"orcontent bind label.
- The React target writes
- The token’s fields as custom properties;
font-sizefor a size token. - Read more
- /language/styling
font-weight
FacetThe weight axis of the role, overridden on its own. It takes a font-weight token or a bare number.
part Demo.title: text { text body weight weight.bold }
- A bare number such as
600is looked up as a token first and taken as the literal only when no token is named that. A Tailwind import produces a token named600, and in such a fileweight 600means that token.
- The React target writes
font-weight.- Read more
- /language/styling
font-family
FacetThe family axis of the role, overridden on its own, pointing at a font-family token. The token carries the fallback, the source and the licence, because a generated package has to ship or load the font and say under what terms.
part Demo.code: text { font font.mono }
- The React target writes
font-family, with the family and its fallback.- Read more
- /language/tokens
font-style: italic
FacetThe one typography axis that has nothing to tokenise, so its value is a word: normal, italic or oblique.
part Demo.quote: text { slant italic }
- The React target writes
font-style.- Read more
- /language/styling
text-overflow: ellipsis, -webkit-line-clamp
Read, not yet drawnTruncating to N lines is a decision about the text, not about the layout, so it is a facet: text.clamp 2. The reader accepts it today; the document does not carry it yet, and no target draws it. A file that states it is read with a note saying so, and the line survives a round trip only in the text.
part Demo.title: text { text.clamp 2 }
- Read more
- /language/styling
white-space, text-wrap
Read, not yet drawnHow text breaks: wrap, nowrap, pretty, balance. The same standing as text.clamp: read by the reader, not yet carried into the document or drawn.
part Demo.title: text { text.wrap balance }
- Read more
- /language/styling
text-align
Not in the languagenothing - the container places the textA text part is placed by the stack it sits in, with align and distribute, like any other child. Centring a label is a layout decision of the parent, and stating it there is what keeps a text part the same object on every target.
- Read more
- /language/layout
#Layout
Layout is propose/choose, the model SwiftUI, Compose and Flutter already use. Flexbox is one way to realize it, and the web generator does the translating.
layout { stack row | column | layers }stack makes a part a container and names the axis its children flow along. layers is the third axis: children drawn on top of one another, the stack sizing to the largest, which is what position: absolute is usually used for. A part with no stack is not a container, and the generator emits no flex box for it.
part Demo.toolbar: container { layout { stack row; gap space.sm; align center } } part Demo.form: container { layout { stack column; gap space.md } }
row-reverseandcolumn-reversedo not exist. Order is the order of the parts.display: inline-flex,inline-blockand the rest of the display values are the target’s choice of element and flow, not a thing the file says.
- The React target writes
display: flexandflex-direction; alayersstack isdisplay: gridwith every child in the same cell.- Read more
- /language/layout
justify-content
LayoutThe main axis: how children are distributed along the direction of the stack. start, center, end, between, around, evenly - the CSS words without the space- prefix and without the flex- prefix, because neither prefix says anything on a platform that has no flexbox.
part Demo.footer: container { layout { stack row; distribute between } }
- In a
layersstack the children share no axis, sodistributeplaces each one horizontally in the cell andalignvertically.
- The React target writes
justify-content.- Read more
- /language/layout
align-items
LayoutThe cross axis: start, center, end, stretch, baseline.
part Demo.row: container { layout { stack row; align baseline } }
- There is no
align-self: a child cannot override its parent’s alignment. A child that needs to sit differently gets its own stack. align-content(how wrapped lines pack) has no word.
- The React target writes
align-items.- Read more
- /language/layout
flex-wrap
LayoutWhether children start a new line when out of room: nowrap, wrap, wrap-reverse.
part Demo.tags: container { layout { stack row; wrap wrap; gap space.xs } }
- The React target writes
flex-wrap.- Read more
- /language/layout
width, height
Layoutsize <width> <height>A child does not have a width; it has an intent per axis, and the container decides. Five intents replace lengths and the grow/shrink/basis triple: fit (size to content, the default), fill (all the room offered), weight(n) (a share against the siblings), fixed(size.sidebar) (exactly a token’s length), ratio(16:9) (follow the other axis).
part Demo.sidebar: container { layout { size fixed(size.sidebar) fill } } part Demo.main: container { layout { size fill fill } } part Demo.thumb: content { layout { size height fit } }
- The axes are positional, width then height.
size fillis the width alone;size height fitnames one axis when the width must stay unstated. 100%isfill;autoisfit; a fixed pixel width isfixed(token). Asizetoken is the length type for widths and heights, distinct fromspacing.
- The React target writes
fit-content,100%across the main axis, a length forfixed; seeflexbelow forfillalong the main axis.- Read more
- /language/layout
fill, or weight(n)On the axis the siblings share, fill takes the leftover room and weight(2) takes twice the share of a weight(1) sibling. The grow/shrink/basis mechanics are the web’s way of computing that, and the generator writes them.
part Demo.split: container { layout { stack row } } part Demo.split.list: container { layout { size weight(1) fill } } part Demo.split.detail: container { layout { size weight(2) fill } }
- The web gives a
fillchildmin-width: 0along the main axis, because a flex item’s minimum is its content and a long label would push its siblings out instead of truncating. Native layout engines do not have this gotcha; the generator carries the fix so the file does not have to. - Across the main axis
fillis the whole span, andweighthas nothing to weigh against, so it reads asfillthere. - In a wrapping stack a
fillchild keeps its content as its basis, so lines still break where they should.
- The React target writes
flex-grow: n,flex-shrink: 1,flex-basis: 0%(orautowhen wrapping), plusmin-width: 0.- Read more
- /language/layout
min(token) max(token) after an intentA clamp on an intent: the axis never shrinks below the one nor grows past the other. The bounds are size tokens.
part Demo.column: container { layout { size fill min(size.min) max(size.max) fit } }
- The React target writes
min-widthandmax-width(or the height pair).- Read more
- /language/layout
aspect-ratio
Layoutratio(w:h) as an axis intentThe axis that follows the other one is the axis that carries the ratio, so the ratio is an intent on that axis: size fill ratio(16:9) is a width that fills and a height that keeps 16:9.
part Demo.video: content { layout { size fill ratio(16:9) } }
- Only one axis may carry a ratio; two would contradict each other, and the second is not emitted.
- The key form
ratio 16 9is read as “the height follows the width” and never written.
- The React target writes
aspect-ratio.- Read more
- /language/layout
display: grid, grid-template-columns
Read, not yet drawngrid - reservedGrid tracks (fr, minmax, auto-fit) are web-leaning concepts with no faithful native equivalent, and a layout concept earns a word only when it means the same thing on every target. grid cols 2 is read and reported, and no target draws it until a second one can.
part Demo.gallery: container { layout { grid cols 3 } }
- Read more
- /language/layout
#Position
Four positions, on logical edges. Coordinates are not among them.
position overlay, inside a layers stackAn absolutely positioned child is placed by coordinates in a box its parent established. The portable half of that is “on top of its siblings, placed in the parent’s box”, and the stack’s align and distribute place it: a badge sits at the top-end of an avatar because the stack says so. The coordinates are what no other platform would read the same way.
part Demo.avatar: container { layout { stack layers; align start; distribute end } } part Demo.avatar.badge: container { layout { position overlay } }
- An
overlayis placed by the stack’salign(vertical) anddistribute(horizontal). There is notop: 4px; an offset is an inset on the stack. - Later children draw on top. There is no
z-index; the order of the parts is the stacking order.
- The React target writes
position: absolutein aposition: relativegrid cell, aligned by the grid’s item properties.- Read more
- /language/layout
position pinned <edge>A pinned part stays at an edge of its scroll container: a toolbar at block-start, a bottom bar at block-end. The edges are logical so the layout survives a right-to-left locale.
part Demo.header: container { layout { position pinned block-start } }
fixed(pinned to the viewport) is not distinguished fromsticky: a part pins to the nearest scroll container, which is what a toolbar means on every platform.- The physical words
top,bottom,leftandrightare read as the logical edge they name left-to-right, and never written. - Figma cannot express a pin and reports it, rather than drawing a frame where the pin would have been.
- The React target writes
position: stickywith the logical inset of the edge set to 0.- Read more
- /language/layout
position bleed <edge>The negative margin trick extends a child past its parent’s padding to the parent’s edge. bleed says that outright, on the edges named, and the generator computes the number from the parent’s inset - so a change to the padding token moves the bleed with it, which the hand-written negative margin never did.
part Demo.card: container { layout { stack column; gap space.md } inset space.lg } part Demo.card.cover: content { layout { position bleed inline block-start } }
- A bleed on an edge where the parent has no inset does nothing, because there is nothing to cancel.
block,inlineandallname both edges of an axis or all four.
- The React target writes
margin-inline-start: calc(-1 * var(--ds-spacing-…))and its siblings, one per bled edge.- Read more
- /language/layout
z-index
Not in the languagenothing - the order of the partsIn a layers stack the later child is on top, as in every z-stack. A number that competes across unrelated stacking contexts has no portable meaning, and the problems it solves on the web (a popover above a sticky header) are the overlay realization’s job, not the file’s.
- Read more
- /language/layout
#States and conditions
A pseudo-class is how the web observes an interaction. The language names the interaction state and lets each target observe it its own way.
state hovered | pressed | focused | focus-visible | disabled | checked { }A state comes from a behaviour, not from a platform: a toggle emits checked, pressability emits pressed, a selection emits selected. The part styles the state by name, and the web generator picks the pseudo-class or data attribute that its realization of the behaviour exposes. pressed rather than active, because active is a state of its own (the current item of a collection).
part Demo.button: control { fill accent state hovered { fill accent-hover } state pressed { fill accent-pressed } state focus-visible { ring accent } state disabled { opacity opacity.disabled } }
- A state the part’s behaviour cannot reach is a note, not a style: a
state checkedon a plain container never fires. - A state can come from a prop:
bind state disabled disabledputs the part into the state while the prop is true. :first-child,:last-child,:nth-child(odd)are the collection statesfirst,last,odd,even.
- The React target writes
:hover,:active,:focus,:focus-visible,:disabled,:checked;[data-selected],[data-open],[data-loading]for the states the element has no pseudo-class for.- Read more
- /language/styling
when variant = secondary { }A modifier class or a data attribute is a prop made visible to a selector. The file conditions on the prop itself, and the generator decides how the prop reaches the stylesheet.
part Demo.button: container { fill accent when variant = ghost { fill transparent ink accent } state hovered when variant = ghost { fill accent-subtle } }
- Predicates combine:
and,or,not, parentheses;=,!=,<,contains. A leaf with no operator means “is set”. - A
whenblock may also carry alayout { }, for a prop that changes the structure (a horizontal or vertical list) and not only the paint. - A value that varies by prop everywhere (a
sizeprop that drives padding, font and radius together) is not a stack ofwhenblocks: declare the prop as acontextdimension and alias the tokens by it.
- The React target writes
- A
data-variantattribute on the element and an attribute selector in the rule. - Read more
- /language/styling
when ancestor <part> <state> { }The selector reads outward from the child; the predicate does too, naming the ancestor part and the state. Stating it on the child keeps the ancestor’s part free of knowledge about who styles against it.
part Demo.icon: icon { ink text-muted when ancestor Demo hovered { ink accent } }
- The React target writes
- A descendant selector:
.demo:hover .icon. - Read more
- /language/styling
context-group ColorScheme + alias by ColorScheme on the tokenDark mode is a dimension of the system, and a dimension lives in foundations: the token answers per context, and the components never mention it. A facet cannot be aliased (fill alias by … is an error) because the alias belongs where it can be shared.
context-group ColorScheme { Light Dark } foundations { layer Base { token white : color = hex(ffffff) token gray/900 : color = hex(0f172a) } layer Semantic { token surface : color = alias by ColorScheme { default -> white; Dark -> gray.900 } } }
- Several dimensions (scheme, density, platform, brand) resolve by chaining single-dimension aliases through the layers, never by a composite key.
- A viewport breakpoint is not a context the language defines. A context group is a dimension the consuming app sets; what changes at a width is a layout decision the app makes about which component it places.
- The React target writes
- A
[data-color-scheme="dark"]block intheme.cssthat redefines the custom properties. - Read more
- /language/tokens
#Values and variables
A CSS custom property is a name for a value. A token is a name, a type and a place in a layer, and the type is what lets every target read it.
--space-sm: 8px; var(--space-sm)
Foundationstoken space/sm : spacing = 8px, referenced as space.smA token is typed: spacing, radius, size, border, font-size, color, duration and the rest. The type is what a facet checks, what the Studio offers in a picker, and what Figma turns into a Variable of the right kind. It is declared with a slash path and referenced with a dotted one.
foundations { layer Base { token space/sm : spacing = 8px token space/lg : spacing = 16px } } component Demo { part Demo: container { inset space.sm space.lg } }
- A length type rejects a bare number:
8px, never8. The unit is what makes it a length. - Tokens live in layers that resolve downward: Base holds the literals, Semantic aliases them by role, and a component layer may alias those again.
- The React target writes
- One custom property per token in
theme.css,--ds-<type>-<id>, andvar(…)wherever a facet points at it. - Read more
- /language/tokens
px, rem, em, %, vw
Foundationspx (density-independent) and scaled()Two kinds of length, both portable. px is not a CSS pixel: it is the density-independent unit Android calls dp, and every target converts it. scaled(1.25) is a length relative to the reader’s text size, which is what rem is for on the web and sp on Android.
token space/md : spacing = 12px token size/text-md : font-size = scaled(1)
- The reader accepts
remandspas spellings ofscaled()anddpas a spelling ofpx; the printer writes the canonical forms. em,%,vwandvhhave no neutral meaning and are not length units of the language. A percentage of a parent isfillorweight; a share of the viewport is the app’s decision.
- Read more
- /language/values
#7c3aed, rgb(), hsl(), oklch(), color()
Foundationshex(), rgb(), srgb(), p3(), oklch(), hsl()A colour literal names its space, so a wide-gamut colour survives a round trip and a target that cannot show it knows what it is approximating. hex(7c3aed) has no #, because the literal is a function like the others.
token violet/600 : color = hex(7c3aed) token violet/glow : color = oklch(0.62 0.19 300) token scrim : color = srgb(0 0 0 0.4)
- A trailing channel is alpha:
hex(ffffffcc),srgb(1 1 1 0.8). - A colour literal goes in a token, never in a facet.
- Read more
- /language/values
calc(), color-mix()
FoundationsA derived value is a token computed from another by steps, and it follows its base when the base changes. Colour steps happen in a named space; length steps scale, offset and round.
token accent : color = hex(7c3aed) token accent-hover : color = compute accent in oklch { darken 10% } token space/md : spacing = 12px token space/lg : spacing = compute space.md { scale 1.5 }
- The maths happens at generation time, so the generated CSS holds the result, not a
calc(). That is what lets a target withoutcalc()read it.
- Read more
- /language/values
an unstated border colour; an unstated facetThere is no currentColor keyword because there is a rule instead: a border with no colour of its own follows the ink, everywhere. inherit is the absence of a facet - a part that states no ink takes whatever its target’s inheritance gives it, which on the web is the parent’s colour.
- The React target writes
currentColorfor the border; the property left unset for the absent facet.- Read more
- /language/styling
!important, specificity, the cascade
Not in the languagenothing - a part states its own facetsThere is no cascade to win. Each part states its facets; a state or when block changes them under its condition; nothing from outside the component reaches in. Consumers restyle through tokens and through the seams the component declares, never by out-specifying its rules.
- Read more
- /thinking
#Motion
A transition names what animates and points at timing tokens. Keyframe animation has no word.
transition
Motionmotion { transition <facets> duration <token> easing <token> }The facets that animate between states, timed by duration and easing tokens. Motion adds no state; the behaviour states are its anchors.
foundations { token fast : duration = 120ms token standard : easing = raw "cubic-bezier(0.2, 0, 0, 1)" } component Demo { part Demo: container { motion { transition fill ink duration fast easing standard } } }
- A transition without a
durationis skipped. - An easing is a
rawstring, because a curve’s spelling is the web’s; a target that cannot read it falls back to its own.
- The React target writes
transitionon the facets’ properties.- Read more
- /language/styling
@keyframes, animation, transform
Not in the languagenothing yetKeyframe animation and transforms are realization work each platform does very differently, and the language has no words for them yet. A spinner or a slide-in is the archetype’s realization (a progress archetype spins; a dialog’s presentation animates) rather than a thing the file draws.
- Read more
- /language/styling
#Visibility and the rest
Properties that are not paint. Most are either a structural statement or a target decision.
display: none, visibility: hidden
Conditionpresent when <condition>Hiding a part is a structural fact - it is not there - and the file says that with a presence condition on the part. A hidden part is not rendered at all, on every target, which visibility: hidden never meant.
part Demo.icon: icon { present when icon }
- Read more
- /language/parts
cursor, user-select, pointer-events
Not in the languagenothing - the behaviour decidesA pointer cursor is what the web shows over a pressable thing; the archetype knows the part is pressable and the web realization sets the cursor. The same for selectability and hit testing: they follow from the behaviour the part composes, not from a property.
- Read more
- /language/behaviors
Whatever is not here
Not in the languagea slot, a per-target option, or foreign contentNo vocabulary covers everything. When the model cannot say what you need, the answer is a seam declared in the document: a slot where consumers put their own content, a per-target option at release time, or, as a last resort, foreign content that carries one target’s code in quarantine, tagged so every other reader skips it honestly. Editing the generated CSS is never the answer: the next release overwrites it.
- Read more
- /thinking
#In the Studio
The Studio's Style tab offers the same facets under the same names - Fill, Ink, Corner, Inset, Border width, Border color, Elevation, Opacity, Focus ring - so this page reads for both front-ends. What the Studio adds is that it offers each part only the facets its kind can take, and every value is picked from the tokens the system declares rather than typed. Its Inset control has three modes, one value, one per axis and one per edge, and each writes the statements the padding entries above describe.
position bleed. A z-index was for a badge over an avatar, and that is a layers stack. A cursor: pointer was for telling people a thing is pressable, and the archetype already knows. The language names the intent; the property was one platform's way of getting there.The words themselves are defined once in the glossary, and every statement a file can contain is in the language reference.