Foundations·00
The token layer
Everything a component is allowed to spend, named once. Fifteen radii that nest inside each other, twelve type steps, two curves, five springs, three materials.
Materials
Everything is a stack of three: the bezel things sit in, the panel lifted out of it, the well cut into that. Depth is real. A border standing in for depth is the thing this replaces.
--bezelthe frame things sit in--panelthe lifted card, where content lives--wellthe recessed slot, where things go in--suba subdued band inside a panelInk and hairlines
Three inks and two hairlines. Both ramps were raised twice: the first pass read as refined in a mockup and as broken on a real screen. When the choice is between subtle and legible, legible wins. Restraint lives in the type sizes and the motion.
--inkheadings, primary text--ink-2body copy--ink-3metadata, labels, disabled rows--hairlinedividers, compartment edges--hairline-stronghover borders, scrollbar thumbAccent, and the two verdicts
Accent marks interaction and state, never decoration. Selection and “on” are said with ink and depth instead: if every selected thing were blue, focus would stop meaning anything. The accent is also chosen against the surface it is drawn on, not against the page, so an inverted button in light mode takes the dark-mode accent.
--accentinteraction, focus, selected state--accent-softtext selection, accent wash--mosssuccess, confirmation--flagrejection: validation errors, failed statesRadii nest
Outer radius = inner radius + the padding between them. Nothing here was picked because it looked right on its own; each number is derived from the one it wraps, which is why the scale is dense at the bottom and why two adjacent boxes never read as almost-concentric.
rounded-pagepage panelrounded-framepreview frame, code block framerounded-cardcard, props table, overlay panel, sectionrounded-card-smlist cardrounded-shelltabs shellrounded-panelwell in a 16 frame, popover, dropdown, row cardrounded-fieldfield, drop target, avatar tilerounded-rowlist row, button, search field, tile in a 12rounded-row-smnested list row, tab plateau, step tilerounded-iconicon button in a panel, avatar wellrounded-capcap, chip, chevron button, small actionrounded-cellcell, small chip, kbd, checkboxrounded-trackprogress trackrounded-fillfill, tick, railrounded-markripple cell, marquee markType
Sans by default, mono only for metadata, numbers, ids and keycaps. Headings are medium, never bold: the negative tracking does the work that weight would do badly. Every step carries its family, weight, leading and tracking with it, so there is no way to spell four of the five right.
Two rules the ramp enforces on your behalf. leading-none is banned on anything containing a word: it shrinks the box to the em square and a descender then sits high in it. The two mono steps are the exemption, because they only ever hold a numeral or a short token in a fixed-height row. And uppercase never ships at default tracking, which is a wall, so the label step carries its 0.08em.
text-displaylanding headlinemaking things feel right
text-titlepage titlemaking things feel right
text-title-2sticky-header expanded titlemaking things feel right
text-ledelede, modal title, otp charmaking things feel right
text-bodybody, panel copymaking things feel right
text-controlcard title, row name, controlmaking things feel right
text-secondarysidebar row, secondary line, chipmaking things feel right
text-captionhint, error, tooltip, captionmaking things feel right
text-labelsection label, column headermaking things feel right
text-dockdock labelmaking things feel right
text-metametadata, countsmaking things feel right
text-idids, counters, keycapsmaking things feel right
Two curves
arrivedecelerates into place, which is what “something arrived and stopped” looks like. leaveis the mirror: it starts slow and accelerates out of frame, which is what “this is over, do not wait for it” looks like. There is no third one.
ease-arrivedecelerates into place: something arrived and stopped
ease-leaveaccelerates out of frame: this is over, do not wait for it
Durations
A departure is always shorter than an arrival. An exit that takes as long as an entrance means the thing replacing it either waits, which reads as lag, or crossfades through it, which reads as a smear. The two modes across the whole component set are 140 and 200; everything else rounds to one of them.
--duration-flashopacity beating its own surface out--duration-exita departure--duration-enteran arrival--duration-settlea surface that travelled a real distance--duration-drawa stroke drawing itselfFive springs
Read the table as a shape rather than a list. Stiffness rises as the distance falls and mass falls the same way, because the distance chooses the spring. Measure the travel: over 200px takes disclose, 20 to 200 takes cell, under 20 takes small. The ζ column is the damping ratio: under 1 overshoots, at 1 is critical.
cellζ 1.11{ stiffness: 520, damping: 34, mass: 0.45 }the default: a cell lighting, a tick landing, a row taking its slot
crossfadeζ 1.18{ stiffness: 260, damping: 34, mass: 0.8 }opacity between two faces, or content being replaced wholesale
smallζ 1.23{ stiffness: 700, damping: 46, mass: 0.5 }a label lifting, a caret rotating, a chip settling
discloseζ 1.10{ stiffness: 150, damping: 27, mass: 1 }a surface travelling a real distance: a drawer, a card flying home
surfaceζ 0.93{ stiffness: 420, damping: 36, mass: 0.9 }the modal panel: alive, does not bounce
Strokes and the icon grids
Icons are Phosphor geometry drawn inline, never imported, so a component still costs one dependency. strokeWidth 16 on a 256 viewBox renders as 1.5px at 24px: the grids are the same line weight, so use whichever the source glyph came from and do not convert.
--stroke-hairlinedividers, compartment edges, the ring in every shadow--stroke-focusthe focus outline, at 1px offsetviewBox 0 0 256 256Phosphor geometry, scaled by width/heightviewBox 0 0 24 24a custom mark on the 24 gridviewBox 0 0 12 12a mark inside a controlLayers
Four stops. A fifth is someone losing an argument rather than a new kind of thing being on screen.
--layer-raisethe fade strips and the page header--layer-headerthe docs pane header, over its own scroll--layer-scrimthe dim behind a drawer--layer-overlaythe drawer, the launcher