Values & literals
In Milda a value knows what it is. Colours are space-aware functions, lengths carry a unit, durations are times, and records have named fields. Because every literal types itself, the language can tell a colour from a length from a time, and flag a value that lands in the wrong slot before anything is generated.
Most of the time you name values as tokens and reference those tokens everywhere else. Literals are how you define a token, and the occasional one-off. This page is the vocabulary of literals themselves.
#Colours are functions, not strings
A leading # starts a comment, so a colour cannot be a #rrggbb string. Colours are written as functions that name their colour space, so the value is unambiguous rather than a string a target has to guess at:
hex(ffffff) # sRGB, RRGGBB; also 3, 4 or 8 digits hex(ffffffcc) # RRGGBBAA - the last byte is alpha rgb(255 0 0) # sRGB, 0-255 channels srgb(1 0 0) # sRGB, 0-1 channels srgb(1 1 1 0.8) # a trailing number is alpha p3(1 0 0) # display-p3, a wider gamut oklch(0.62 0.19 256) # OKLCH: lightness, chroma, hue hsl(210 1 0.5) # HSL transparent # no colour at all
The spaces are hex, rgb, srgb, p3, oklch and hsl (rgba and hsla are read as the same). They round-trip exactly: the parser and the printer are symmetric, so the colour you write is the colour that comes back out.
#Lengths carry their units
A number with a unit is a length, and the unit says what kind. Two kinds exist, and both are platform-neutral:
8px # a density-independent length; the canonical spelling scaled(1.25) # a length relative to the reader's text size 0px # zero still carries a unit pill # a radius that fully rounds the shorter side
px is not a CSS pixel: it is the unit Android calls dp, and every target converts it. The reader also accepts dp as a spelling of px, and rem and sp as spellings of scaled(), for authors arriving from the web and from Android; the printer writes the canonical forms. A length type (spacing, radius, size, border, font-size, letter-spacing) rejects a bare number: the unit is what makes it a length.
#Durations are times
Time units make a duration, used by motion tokens and transitions:
token fast : duration = 120ms token slow : duration = 0.4s
#Numbers that are not lengths
Three types take a bare number, and the type word is what keeps the number from being read as a length: a font weight, a ratio (a line height is a multiple of the text size, never a length), and an opacity.
token weight/medium : font-weight = 500 token leading/normal : ratio = 1.5 token dim : opacity = 0.5
Older files spell the ratio type line-height or number. Both are still read; ratio is what Milda writes, and it earned its own surface version because a reader that does not know the word would otherwise read 1.5 as 1.5px.
#Records: a font family and a text style
Two token types are records, written as fields in braces. A font-family says where the family comes from, which is what a generator needs to decide whether to link a stylesheet, write an @font-face or do nothing. A typography token bundles the axes of a text style, and its family may be a font-family token's path, so the style stays tied to a family that can differ per context:
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; letter-spacing: 0px }
The reading rule for a field that takes either: a structure is the value, a bare path is the token. A part paints with the whole style through the text facet and overrides one axis with font, weight or slant; see Facets.
#Computed values
A token can be derived from another, live, with steps that follow its type: lighten, darken, saturate, desaturate, alpha, rotate and mix for a colour, in a chosen colour space; scale, offset and round for a length. When the base changes, the result follows. Where a computed token sits among the layers is on Tokens & context.
token accent-hover : color = compute accent in oklch { darken 10% } token space/lg : spacing = compute space.sm { scale 2 }
#Literals are self-typing
Because each literal carries its own kind, the language checks it against the token's declared type. A colour in a length slot, or a length where a colour belongs, is caught before anything is generated:
token bad : color = 8px # error: a length literal in a colour slot
hex(...) differently from a keyword, without running a generator.#Opaque values and the escape hatch
Two types have no neutral literal yet and are written as an opaque string with : a shadow and an easing curve. The reader carries the string and each target renders it as it can.
token shadow/card : elevation = raw "0 4px 12px -2px rgba(15,23,42,0.12)" token standard : easing = raw "cubic-bezier(0.2, 0, 0, 1)"
When a value genuinely cannot be said in the language at all, carries a platform string verbatim and marks the token as partial: a target that cannot read the string has nothing to fall back on, so the note says why. It is a deliberate, visible escape hatch, never the default.
token tint : color = escape "color-mix(in srgb, red 40%, blue)" note "web only, until compute can mix in srgb"
These words, in the reference
- A value written as itself.
- An explicit typed value.
- A platform-specific value the language cannot express, carried verbatim.
Open a word for its entry, or read them all in the reference.
See it built in the tour1. A file and a token10. Type, motion and computed values