Lesson 10 of 14
Type, motion and computed values
A font family and text styles, a transition, and a hover colour derived from the accent.
#The idea
Three things a system says once and uses everywhere: what its text looks like, how its surfaces move between states, and which values are derived from which. Each is a token with a shape of its own.
#What this lesson adds
milda
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 title : typography = { family: font.sans; size: 18px; weight: 700; line-height: 1.3 } token shadow/card : elevation = raw "0 4px 12px -2px rgba(15,23,42,0.12)" token fast : duration = 120ms token standard : easing = raw "cubic-bezier(0.2, 0, 0, 1)"
milda
token accent-hover : color = compute accent in oklch { darken 10% }
milda
motion { transition fill ink duration fast easing standard }
#What to notice
- A font family is a record, not a CSS stack. family, fallback,
source(system, google or custom) and licence. The source is what a generator needs to know whether to link a stylesheet, write an@font-face, or do nothing. - A text style bundles the axes - family, size, weight, line height, letter spacing - and
family: font.sansis a token path, so the style stays tied to a family that can differ per context. A part paints withtext body; and override one axis of the role. - A computed value follows its base.
compute accent in oklch { darken 10% }replaces the hand-picked hover colour: change the accent and the hover follows. The steps a token accepts follow its type, so on a length is an error. - Motion decorates transitions. A names the facets that animate and the tokens that time them. It adds no state; the behaviour states from lesson 4 are the anchors.
- Elevation is honest about being platform-shaped. The shadow here is a string, which a reader for another target carries without claiming to understand. A system that targets several platforms aliases an elevation token
by Platforminstead, so the web gets a shadow and Android a tonal step.
#What the generator makes of it
The theme imports the Google stylesheet for Inter and sets the family on :root; the hover colour is written as the oklch() the computation produced; the Button's CSS gains a of background and colour timed by the two tokens.
#Try it
Change darken 10% to lighten 20%, then change the accent itself. Then add slant italic to the Card title.
Words in this lesson
- Declares a design token.
- An explicit typed value.
- A value derived from another one, live: "the accent, 20% lighter".
- How the part animates between its states.
Open a word for its entry, or read them all in the reference.
#The file at the end of this lesson
milda
milda 0.10 prelude 0.0.2 meta { name "Acme" } context-group ColorScheme { Light Dark } context-group Variant { Primary Secondary Ghost } group Actions "Things a person presses to make something happen." group Forms "Collecting input." group Feedback "Telling a person what is happening." group Layout "Surfaces that hold the rest." foundations { layer Base { token violet/100 : color = hex(ede9fe) token violet/500 : color = hex(8b5cf6) token violet/600 : color = hex(7c3aed) token violet/700 : color = hex(6d28d9) token violet/900 : color = hex(4c1d95) token gray/50 : color = hex(f8fafc) token gray/200 : color = hex(e2e8f0) token gray/500 : color = hex(64748b) token gray/800 : color = hex(1e293b) token gray/900 : color = hex(0f172a) token green/500 : color = hex(22c55e) token amber/500 : color = hex(f59e0b) token white : color = hex(ffffff) } layer Semantic { token accent : color = alias by ColorScheme { default -> violet.600; Dark -> violet.500 } token accent-hover : color = compute accent in oklch { darken 10% } token accent-subtle : color = alias by ColorScheme { default -> violet.100; Dark -> violet.900 } token on-accent : color = alias by ColorScheme { default -> white } token surface : color = alias by ColorScheme { default -> white; Dark -> gray.900 } token text : color = alias by ColorScheme { default -> gray.900; Dark -> gray.50 } token text-muted : color = alias by ColorScheme { default -> gray.500 } token line : color = alias by ColorScheme { default -> gray.200; Dark -> gray.800 } token success : color = alias by ColorScheme { default -> green.500 } token warning : color = alias by ColorScheme { default -> amber.500 } token space/xs : spacing = 4px token space/sm : spacing = 8px token space/md : spacing = 12px token space/lg : spacing = 16px token space/xl : spacing = 24px token radius/sm : radius = 6px token radius/lg : radius = 12px token radius/pill : radius = pill token hairline : border = 1px token size/sm : font-size = 13px token size/md : font-size = 15px token size/lg : font-size = 18px token weight/medium : font-weight = 500 token weight/bold : font-weight = 700 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 title : typography = { family: font.sans; size: 18px; weight: 700; line-height: 1.3 } token shadow/card : elevation = raw "0 4px 12px -2px rgba(15,23,42,0.12)" token fast : duration = 120ms token standard : easing = raw "cubic-bezier(0.2, 0, 0, 1)" } } component Button "The primary action, in three emphases." { archetype Button group Actions props { variant : context Variant = Primary "Which emphasis the button carries." disabled : boolean = false loading : boolean = false "Shows a spinner in place of the label." loadingLabel : string required when loading "Read out while the spinner replaces the label." label : string = "Get started" icon : string "A glyph shown before the label, by name." } events { click } part Button: container { emits { click on activate } bind state disabled disabled bind state loading loading layout { stack row; gap space.sm; align center; distribute center } fill accent ink on-accent border accent hairline corner radius.sm inset space.sm space.lg when variant = Secondary { fill surface ink text border line hairline } when variant = Ghost { fill surface ink accent border surface hairline } state hovered { fill accent-hover border accent-hover hairline } state hovered when variant = Secondary { fill accent-subtle } state hovered when variant = Ghost { fill accent-subtle border accent-subtle hairline } state focus-visible { ring accent } state disabled { opacity 50 } motion { transition fill ink duration fast easing standard } } part icon: icon { content bind icon ink on-accent when variant = Secondary { ink text } when variant = Ghost { ink accent } } part label: text { slot of content default content bind label text body weight weight.medium } } component Badge "A small status pill." { archetype Badge group Feedback props { tone : enum(neutral, success, warning) = neutral label : string = "New" } part Badge: container { layout { stack row; align center } fill accent-subtle ink accent corner radius.pill inset space.xs space.sm when tone = success { fill success ink on-accent } when tone = warning { fill warning ink gray.900 } } part label: text { slot of content default content bind label text size.sm weight weight.bold } } component Card "A surface composed from the components above." { group Layout props { tone : enum(neutral, success, warning) = neutral } part Card: container { layout { stack column; gap space.md; size fill fit } fill surface ink text border line hairline corner radius.lg inset space.xl elevation shadow.card } part Header: container { layout { stack row; gap space.sm; align center; distribute between } } part Header.Title: text { content "Project Aurora" text title } part Header.Status: instance { instance Badge { label = "Beta" tone = bind tone } } part Body: text { slot of content default ink text-muted text body } part Footer: container { layout { stack row; gap space.sm; distribute end } } part Footer.Primary: instance { instance Button { label = "Open" } } part Footer.Secondary: instance { instance Button { label = "Share" variant = "Secondary" } } } component TextField "A single-line input with an error state." { archetype TextField group Forms props { label : string required "What the field asks for. Also its accessible name." value : string placeholder : string = "you@example.com" invalid : boolean = false error : string "Shown under the control while invalid." disabled : boolean = false } events { valueChange } part TextField: container { bind state disabled disabled bind state error invalid layout { stack column; gap space.xs } } part label: text { content bind label text size.sm weight weight.medium } part control: input { content bind placeholder emits { valueChange on input payload bind value } bind value value value string a11y { labelled-by label; described-by hint } layout { size fill } fill surface ink text border line hairline corner radius.sm inset space.sm space.md text size.md state focus-visible { border accent hairline } state error { border warning hairline } state disabled { opacity 50 } } part hint: text { present when invalid content bind error ink warning text size.sm } } component Switch "One boolean, as a sliding switch." { archetype BooleanInput group Forms props { checked : boolean = false "Whether it is on." label : string required disabled : boolean = false } events { checkedChange } part Switch: container { bind state disabled disabled layout { stack row; gap space.sm; align center } } part toggle: input { control switch a11y { name bind label } emits { checkedChange on change payload bind checked } bind value checked value boolean } } component Dialog "A blocking layer that asks one question." { archetype Dialog group Feedback uses presentation(surface: dialog) as P uses dismiss as D wire D.dismissRequested -> P.close props { trigger : string = "Open" "The label of the control that opens the dialog." title : string required "The dialog's accessible name." open : boolean = false } part Dialog: container { bind state open open } part trigger: control { layout { stack row; gap space.sm; align center; distribute center } fill accent ink on-accent corner radius.sm inset space.sm space.lg state focus-visible { ring accent } } part trigger.label: text { content bind trigger weight weight.medium } part backdrop: container { fill text opacity 50 } part surface: container { layout { stack column; gap space.md } fill surface ink text corner radius.lg inset space.xl } part surface.title: text { content bind title text size.lg weight weight.bold } part surface.content: container { slot of content default ink text-muted } }