Tokens

51 level 1 tokens that a skin redefines, and 101 structural ones it does not. Every specimen below renders with var(--token), so the toggle in the top bar re-renders this page rather than describing it.

Level 1 — what a skin defines

These are the tokens in the skin contract. Each row shows the live value under the skin and mode you are reading in, then the two literals each shipped skin declares — the halves of its light-dark(), which is what you would copy as a starting point for a new one.

Typography

Four families. Which is used where is a component decision.

Typography tokens
LiveTokenPurposedefault light / dark

Sphinx of black quartz, judge my vow — 0123456789

--font-primaryheadingssystem-ui, sans-serif / system-ui, sans-serif

Sphinx of black quartz, judge my vow — 0123456789

--font-secondarybody copy and UIsystem-ui, sans-serif / system-ui, sans-serif

Sphinx of black quartz, judge my vow — 0123456789

--font-tertiarydecorative, accents, pull quotesGeorgia, serif / Georgia, serif

Sphinx of black quartz, judge my vow — 0123456789

--font-monocode, tabular figures, keyboard keysui-monospace, SFMono-Regular, Menlo, monospace / ui-monospace, SFMono-Regular, Menlo, monospace

Surfaces and content

Surfaces and content tokens
LiveTokenPurposedefault light / dark
--color-surfacepage backgroundoklch(98.5% 0.002 260) / oklch(16% 0.003 260)
--color-surface-raisedcards, popovers, anything above the pageoklch(100% 0 260) / oklch(24% 0.004 260)
--color-surface-sunkeninputs, wells, inset regionsoklch(94.5% 0.005 260) / oklch(0% 0 260)
--color-surface-inversehigh-contrast blocksoklch(22% 0.012 260) / oklch(96% 0.003 260)
--color-on-surfaceprimary textoklch(15% 0.005 260) / oklch(100% 0 260)
--color-on-surface-mutedsecondary textoklch(33% 0.007 260) / oklch(87% 0.005 260)
--color-on-surface-subtleplaceholders, disabled textoklch(38% 0.007 260) / oklch(82% 0.006 260)
--color-on-surface-inversetext on --color-surface-inverseoklch(98% 0.002 260) / oklch(18% 0.012 260)

Borders

Borders tokens
LiveTokenPurposedefault light / dark
--color-borderdefaultoklch(58% 0.009 260) / oklch(62% 0.008 260)
--color-border-subtledividers, low-emphasis separationoklch(72% 0.008 260) / oklch(46% 0.008 260)
--color-border-strongemphasis, focus-adjacentoklch(38% 0.008 260) / oklch(82% 0.006 260)

Brand roles

Each with a hover, an active and a foreground.

Brand roles tokens
LiveTokenPurposedefault light / dark
--color-primary—oklch(42% 0.19 260.6) / oklch(80% 0.13 251.5)
--color-primary-hover—oklch(37% 0.17 260.6) / oklch(85% 0.1 251.2)
--color-primary-active—oklch(32% 0.15 260.6) / oklch(90% 0.07 251.3)
--color-on-primary—oklch(99% 0 0) / oklch(17% 0.03 258)
--color-secondary—oklch(42% 0.2 279.3) / oklch(78% 0.13 274.7)
--color-secondary-hover—oklch(37% 0.19 279.2) / oklch(83% 0.1 274.4)
--color-secondary-active—oklch(32% 0.17 279.3) / oklch(88% 0.08 274.7)
--color-on-secondary—oklch(99% 0 0) / oklch(17% 0.03 300)
--color-accent—oklch(45% 0.13 46) / oklch(84% 0.12 57.1)
--color-accent-hover—oklch(40% 0.12 45.9) / oklch(88% 0.1 59.7)
--color-accent-active—oklch(35% 0.1 46) / oklch(92% 0.08 58.8)
--color-on-accent—oklch(16% 0.025 65) / oklch(18% 0.03 65)

Status roles

A solid colour, a foreground for it, and a tinted surface.

Status roles tokens
LiveTokenPurposedefault light / dark
--color-info—oklch(44% 0.1 234.2) / oklch(86% 0.11 221.8)
--color-on-info—oklch(99% 0 0) / oklch(17% 0.03 240)
--color-info-surface—oklch(96% 0.025 240) / oklch(26% 0.05 240)
--color-success—oklch(43% 0.13 147.4) / oklch(84% 0.18 147)
--color-on-success—oklch(99% 0 0) / oklch(16% 0.03 150)
--color-success-surface—oklch(96% 0.03 150) / oklch(25% 0.05 150)
--color-warning—oklch(45% 0.1 73.2) / oklch(92% 0.16 94.9)
--color-on-warning—oklch(99% 0.01 75) / oklch(18% 0.03 75)
--color-warning-surface—oklch(93.5% 0.055 75) / oklch(28% 0.06 75)
--color-danger—oklch(45% 0.19 24.6) / oklch(76% 0.18 25.1)
--color-on-danger—oklch(99% 0 0) / oklch(16% 0.03 25)
--color-danger-surface—oklch(96% 0.03 25) / oklch(27% 0.07 25)

Utility

Utility tokens
LiveTokenPurposedefault light / dark
--color-focusfocus ringoklch(42% 0.19 260.6) / oklch(80% 0.13 251.5)
--color-shadowthe colour elevation is built fromoklch(30% 0.02 260) / oklch(0% 0 0)
--color-overlaydialog and drawer backdropsoklch(22% 0.012 260 / 0.5) / oklch(8% 0.01 260 / 0.65)
--color-selection::selection backgroundoklch(88% 0.08 258) / oklch(40% 0.12 258)

Shape

Shape tokens
LiveTokenPurposedefault light / dark
--radius-none—0 / 0
--radius-sm—0.25rem / 0.25rem
--radius-md—0.5rem / 0.5rem
--radius-lg—0.75rem / 0.75rem
--radius-xl—1rem / 1rem
--radius-full—9999px / 9999px
--border-width—2px / 2px
--border-width-strong—2px / 2px

All 51 tokens are declared by the skin that ships, in both modes. That is not a coincidence you have to trust: this page is generated by reading the contract and each skin file, and a token missing from a skin would show as — in that column.

Structural — what stays the same across skins

The rhythm, the type scale, motion and elevation geometry are not thematic. A new skin is not a reason to change what --space-4 means. The one exception is --space-scale, which a skin may set to change density.

Spacing scale

Structural, not thematic: the rhythm stays the same across themes. What a theme (or a section, or a single component) may change is --space-scale, which multiplies the whole scale at once. That multiplier is what "density" means here — compact and comfortable are not separate features, they are --space-scale: 0.8 and --space-scale: 1. DENSITY IS AN ATTRIBUTE, NOT A LOOSE NUMBER This file used to say «because the multiplier is inherited, you can make one region denser without touching anything else: .sidebar { --space-scale: 0.85 }». That never worked. The steps below are declared on :root, so their var(--space-scale) is substituted there and every descendant inherits a resolved length; a --space-scale written further down has nothing left to multiply. Measured 17 August 2026: --space-4 came out 16px at scale 1, at 0.5 and at 2 alike. It is the same trap _contract.css records for scoped skins, and the same answer — the tokens have to be re-declared where the override is made. <aside data-density="compact">…</aside> [data-density] re-declares the whole scale, so the multiplier resolves on that element and the region below it is denser. A closed set rather than a free number, for the reason Cloudscape gives: density is a system-level decision, and a scale anyone can write anywhere compounds — 0,85 inside 0,85 is 0,72, and nobody chose that.

Spacing scale tokens
LiveTokenValuePurpose
--space-unit0.25remThe atom every step is built from. Change this and the whole UI rescales.

Unitless multiplier, currently ×1

--space-scale1Density multiplier. Inherited, so it can be scoped to any subtree.
--space-00—
--space-px1px—
--space-1max(2px, calc(var(--space-unit) * 1 * var(--space-scale)))—
--space-2calc(var(--space-unit) * 2 * var(--space-scale))—
--space-3calc(var(--space-unit) * 3 * var(--space-scale))—
--space-4calc(var(--space-unit) * 4 * var(--space-scale))—
--space-5calc(var(--space-unit) * 5 * var(--space-scale))—
--space-6calc(var(--space-unit) * 6 * var(--space-scale))—
--space-8calc(var(--space-unit) * 8 * var(--space-scale))—
--space-10calc(var(--space-unit) * 10 * var(--space-scale))—
--space-12calc(var(--space-unit) * 12 * var(--space-scale))—
--space-16calc(var(--space-unit) * 16 * var(--space-scale))—
--space-20calc(var(--space-unit) * 20 * var(--space-scale))—
--space-24calc(var(--space-unit) * 24 * var(--space-scale))—
--space-32calc(var(--space-unit) * 32 * var(--space-scale))—
—--rhythm-pagecalc(var(--space-unit) * 25 * var(--space-scale))—
—--rhythm-sectioncalc(var(--space-unit) * 12.5 * var(--space-scale))—
—--rhythm-blockcalc(var(--space-unit) * 6 * var(--space-scale))—
—--rhythm-linecalc(var(--space-unit) * 4 * var(--space-scale))—
—--scroll-padding6.25remHow far above an anchor target the viewport stops. Set this to the height of a sticky header so in-page links do not land underneath it.

Source: src/css/tokens/spacing.css

Typographic scale

Structural: the sizes and rhythm live here and do not change per theme. The font families do change per theme, and are defined in themes/ instead — see themes/_contract.css for the four families every theme must provide. Sizes are fluid. Each step interpolates between a minimum and a maximum across the viewport using vi (the inline-axis viewport unit), so they keep working in vertical writing modes. There are no breakpoints to maintain. The clamp() minimum is always in rem so browser zoom and user font-size settings keep working — never lock a font size to a viewport unit alone.

Typographic scale tokens
LiveTokenValuePurpose

Grumpy wizards make toxic brew

--font-size-xsclamp(0.75rem, 0.73rem + 0.1vi, 0.8125rem)—

Grumpy wizards make toxic brew

--font-size-smclamp(0.875rem, 0.85rem + 0.125vi, 0.9375rem)—

Grumpy wizards make toxic brew

--font-size-base1rem—

Grumpy wizards make toxic brew

--font-size-lgclamp(1.125rem, 1.06rem + 0.32vi, 1.3125rem)—

Grumpy wizards make toxic brew

--font-size-xlclamp(1.375rem, 1.26rem + 0.57vi, 1.75rem)—

Grumpy wizards make toxic brew

--font-size-2xlclamp(1.75rem, 1.55rem + 1vi, 2.375rem)—

Grumpy wizards make toxic brew

--font-size-3xlclamp(2.25rem, 1.9rem + 1.75vi, 3.25rem)—

Grumpy wizards make toxic brew

--font-size-4xlclamp(2.875rem, 2.3rem + 2.9vi, 4.5rem)—

Two lines of text, so the leading between them is the thing you are looking at rather than something you have to imagine.

--line-height-none1—

Two lines of text, so the leading between them is the thing you are looking at rather than something you have to imagine.

--line-height-tight1.15—

Two lines of text, so the leading between them is the thing you are looking at rather than something you have to imagine.

--line-height-snug1.35—

Two lines of text, so the leading between them is the thing you are looking at rather than something you have to imagine.

--line-height-normal1.6—

Two lines of text, so the leading between them is the thing you are looking at rather than something you have to imagine.

--line-height-relaxed1.8—

Tracking changes the colour of a paragraph

--letter-spacing-tighter-0.03em—

Tracking changes the colour of a paragraph

--letter-spacing-tight-0.015em—

Tracking changes the colour of a paragraph

--letter-spacing-normal0em—

Tracking changes the colour of a paragraph

--letter-spacing-wide0.025em—

Tracking changes the colour of a paragraph

--letter-spacing-wider0.08em—

Tracking changes the colour of a paragraph

--letter-spacing-bodyvar(--letter-spacing-normal)—

Tracking changes the colour of a paragraph

--letter-spacing-titlevar(--letter-spacing-tight)—

Tracking changes the colour of a paragraph

--letter-spacing-sectionvar(--letter-spacing-tight)—

Tracking changes the colour of a paragraph

--letter-spacing-subsectionvar(--letter-spacing-tight)—

Grumpy wizards make toxic brew

--font-weight-normal400—

Grumpy wizards make toxic brew

--font-weight-medium500—

Grumpy wizards make toxic brew

--font-weight-semibold600—

Grumpy wizards make toxic brew

--font-weight-bold700—
--measure65ch—
--measure-narrow45ch—
--measure-wide80ch—

Source: src/css/tokens/typography.css

Motion

Structural. Durations collapse to near-zero under prefers-reduced-motion, which is handled here once rather than in every component: as long as a component animates using these tokens, it respects the preference for free. Near-zero rather than zero, because a 0s transition never fires transitionend, and some components rely on that event to clean up. SPRINGS, AND WHY THEY ARE NOT BÉZIERS Apple's interfaces settle rather than ease. A bézier is a shape somebody drew; a spring is what the shape becomes once you have said how quickly the thing should respond and how much friction it has. The difference is not subtle at the end of the movement, which is where a bézier arrives dead and a spring arrives alive, and it is the reason a cubic-bezier(0.34, 1.56, …) — which is what this file used to ship — reads as a cartoon bounce rather than as weight. linear() is what makes the real thing expressible. Each curve below is SwiftUI's own spring, sampled: omega = 2π / response zeta = dampingFraction x(t) = 1 − e^(−ζωt)·(cos(ω_d t) + (ζω/ω_d)·sin(ω_d t)) integrated at 27 stops and cut where the curve comes within a thousandth of rest and stays there — which is also where the matching --duration-* comes from. The names are SwiftUI's, so .smooth, .snappy and .bouncy mean here what they mean there. WHICH ONE TO REACH FOR --ease-quick a press, a hover, a checkbox. 300ms response, barely overshoots. The one most interactions want. --ease-snappy a sheet, a popover, a drawer arriving. The default. --ease-smooth anything leaving. It is the only one that does not overshoot, and an exit that overshoots pushes the element back on screen for a frame before it goes. --ease-bouncy a toggle, a like, something that should feel pleased with itself. Deliberately the loudest thing here; used sparingly, per the HIG's "don't add motion for the sake of motion". tests/e2e/motion.spec.ts pins the three things about these that are invisible on screen: that every curve starts at 0 and ends at exactly 1, that the calm one does not overshoot and the bouncy one does, and that the reduced-motion preference reaches the easings and not only the durations.

Motion tokens
LiveTokenValuePurpose
--duration-instant70ms—
--duration-fast130ms—
--duration-normal220ms—
--duration-slow400ms—
--duration-slower700ms—
--duration-quick400ms—
--duration-snappy700ms—
--duration-smooth736ms—
--duration-bouncy820ms—
--ease-linearlinear—
--ease-incubic-bezier(0.4, 0, 1, 1)—
--ease-outcubic-bezier(0, 0, 0.2, 1)—
--ease-in-outcubic-bezier(0.4, 0, 0.2, 1)—
--ease-quicklinear( 0, 0.0428, 0.1416, 0.2639, 0.3899, 0.5081, 0.6127, 0.7015, 0.7745, 0.833, 0.8788, 0.9139, 0.9403, 0.9598, 0.9738, 0.9837, 0.9905, 0.9951, 0.998, 0.9998, 1.0008, 1.0013, 1.0015, 1.0015, 1.0014, 1.0012, 1 )response 0.3s, damping 0.9 — peak 1.002
--ease-snappylinear( 0, 0.047, 0.1552, 0.2887, 0.425, 0.5511, 0.6607, 0.7516, 0.8241, 0.88, 0.9219, 0.9522, 0.9733, 0.9875, 0.9966, 1.002, 1.0049, 1.0061, 1.0063, 1.0058, 1.0051, 1.0042, 1.0034, 1.0026, 1.0019, 1.0014, 1 )response 0.5s, damping 0.85 — peak 1.006
--ease-smoothlinear( 0, 0.0501, 0.1598, 0.2889, 0.416, 0.5308, 0.6291, 0.7107, 0.7766, 0.829, 0.87, 0.9018, 0.9262, 0.9448, 0.9589, 0.9695, 0.9774, 0.9833, 0.9877, 0.991, 0.9934, 0.9952, 0.9965, 0.9974, 0.9981, 0.9986, 1 )response 0.5s, critically damped — never crosses 1, which is the point
--ease-bouncylinear( 0, 0.065, 0.2136, 0.3925, 0.5675, 0.7194, 0.84, 0.9283, 0.9875, 1.023, 1.0407, 1.046, 1.0435, 1.0369, 1.0288, 1.0207, 1.0135, 1.0078, 1.0036, 1.0008, 0.9991, 0.9982, 0.9979, 0.998, 0.9983, 0.9986, 1 )response 0.5s, damping 0.7 — peak 1.046, and it dips under 1 on the way back, which is what stops it reading as a single elastic snap
--ease-springvar(--ease-snappy)Kept because it is a published name, and pointed at the spring that replaces it rather than deleted.

Source: src/css/tokens/motion.css

Elevation

The geometry of each step lives here and is structural. The colour comes from --color-shadow, which every theme defines — so a dark theme gets shadows that read correctly without needing a different elevation mechanism. This deliberately keeps one mechanism across all themes. Swapping shadows for glowing borders in dark mode is a real technique, but it makes every component's elevation conditional on the theme, and that cost is not worth paying for a library that must support an unbounded number of themes. Opacity is baked into each step rather than exposed, because a shadow's alpha is not something anyone tunes independently of its blur.

Elevation tokens
LiveTokenValuePurpose
--elevation-0none—
--elevation-10 1px 2px rgb(from var(--color-shadow) r g b / 0.06), 0 1px 3px rgb(from var(--color-shadow) r g b / 0.1)—
--elevation-20 2px 4px rgb(from var(--color-shadow) r g b / 0.06), 0 4px 8px rgb(from var(--color-shadow) r g b / 0.1)—
--elevation-30 4px 8px rgb(from var(--color-shadow) r g b / 0.06), 0 8px 16px rgb(from var(--color-shadow) r g b / 0.11)—
--elevation-40 8px 16px rgb(from var(--color-shadow) r g b / 0.07), 0 16px 32px rgb(from var(--color-shadow) r g b / 0.12)—
--elevation-50 16px 32px rgb(from var(--color-shadow) r g b / 0.08), 0 32px 64px rgb(from var(--color-shadow) r g b / 0.14)—
--elevation-overlayvar(--elevation-4)For elements sitting in the top layer: dialogs, popovers, menus.

Source: src/css/tokens/elevation.css

Shape roles

The radius scale (--radius-sm … --radius-full) is thematic and lives in themes/. These three are the semantic roles built on top of it: what a selector, a field and a box are shaped like. They are derived, so restating them in every skin would be three lines of copied boilerplate that never vary — a skin should only have to state what it actually changes. A skin may still override any of them (a brutalist skin with square boxes and pill-shaped fields is a legitimate thing to want). :where() keeps the specificity at zero so overriding takes nothing more than restating it. WHY THE SELECTOR IS :where(:root, [data-skin]) AND NOT JUST :root Custom properties are substituted at computed-value time on the element that declares them, and the result is what descendants inherit. If these were declared only on :root, var(--radius-md) would resolve once against the root's scale, and a scoped skin — <div data-skin="sepia"> inside a default page — would inherit the root's radii instead of sepia's. Re-declaring on every skinned element makes each one resolve against its own scale.

Shape roles tokens
LiveTokenValuePurpose
--radius-selectorvar(--radius-sm)checkboxes, radios, switches
--radius-fieldvar(--radius-md)inputs, buttons — anything you type into or press
--radius-boxvar(--radius-lg)cards, dialogs, panels
—--focus-ring-width3px—
—--focus-ringvar(--focus-ring-width) solid var(--color-focus)—
—--focus-ring-offset2px—

Source: src/css/tokens/shape.css

Materials

A material is a surface that lets what is behind it show through. Apple's platforms use them to say *this is a control layer, and the content is underneath* — a toolbar, a sidebar, a sheet, a menu. The Human Interface Guidelines put it in one sentence: «By allowing color to pass through from background to foreground, a material establishes visual hierarchy to help people more easily retain a sense of place.» That is the whole reason a translucent bar is worth the trouble. It is not a decorative frosted-glass effect: it is the only cheap way for a fixed bar to tell you there is content scrolling under it rather than behind a wall. FOUR THICKNESSES, AND THE TRADE THEY MAKE The HIG names the trade explicitly, and it is the reason there is more than one: «Thicker materials, which are more opaque, can provide better contrast for text and other elements with fine features. Thinner materials, which are more translucent, can help people retain their context.» --material-ultra-thin the most context, the least contrast. Over media. --material-thin a bar over a page that is mostly text. --material-regular the default. Menus, popovers, sheets, navbars. --material-thick where a lot of small text sits on it. Sidebars. DERIVED, NOT DECLARED — AND WHY THAT IS NOT A BREACH OF RULE 5 «No runtime colour derivation» is about a theme's colours: a skin states what its blue is rather than asking the library to lighten one. The alpha here is not a colour, it is the material's thickness, which is structural and the same in every skin — so each material is the skin's own raised surface with a structural alpha over it, exactly the way elevation.css already builds every shadow from --color-shadow and a structural opacity. A skin that changes --color-surface-raised gets four correct materials for free, and one that wants different ones restates these four names. THE DEGRADATION LIVES HERE, NOT IN THE COMPONENTS This is the part worth copying. A component opts in with two lines: .navbar { --navbar-bg: var(--material-regular); backdrop-filter: var(--material-filter); } and never writes an @supports, a prefers-reduced-transparency or a [data-legacy] of its own. Four conditions turn every material back into a plain opaque surface, and they are decided once, here: 1. No backdrop-filter. A translucent bar with nothing blurring behind it is not a weaker version of the effect, it is unreadable text over whatever scrolled under it. So the base rule is opaque and the translucency arrives only inside @supports. This is the dual-branch rule from CLAUDE.md, and it is the case it was written for. 2. prefers-reduced-transparency: reduce. Settings ▸ Accessibility ▸ Display ▸ Reduce Transparency. A reader who has asked for this has asked for exactly this, and the platform honours it by making every material opaque rather than by making it slightly less see-through. 3. prefers-contrast: more. The HIG notes that materials change appearance under increased contrast; the honest answer for a library that cannot measure the content behind the glass is to stop guessing. 4. [data-legacy]. The library-wide opt out. tests/e2e/materials.spec.ts pins all four, on real components, because every one of them is a state no screenshot is taken in. SATURATION IS NOT A FLOURISH saturate(180%) next to the blur is what keeps colour alive through the material. Blur averages the pixels behind it, and averaging colours pulls them towards grey; without the boost a blue image under a white material reads as a grey smudge, which is the opposite of «allowing color to pass through». Apple's own materials do the same thing, which is why a photo under a toolbar still looks like a photo.

Materials tokens
LiveTokenValuePurpose
--material-ultra-thinvar(--color-surface-raised)—
--material-thinvar(--color-surface-raised)—
--material-regularvar(--color-surface-raised)—
--material-thickvar(--color-surface-raised)—
--material-filternone—
—--material-ultra-thin-alpha0.55—
—--material-thin-alpha0.7—
—--material-regular-alpha0.82—
—--material-thick-alpha0.93—
—--material-blur20px—
—--material-saturate180%—

Source: src/css/tokens/materials.css

Container widths, and the floor under anything you tap

--measure and friends in typography.css answer "how wide should a line of text be", in ch, and they are about reading comfort. These answer "how wide is a content column", in rem, and they are about page structure. Both are design-system decisions rather than per-page ones, which is why they are tokens — unlike a gap, which is genuinely local and stays inline. They used to say they were deliberately not a container component, on the grounds that using one is max-inline-size: var(--container); margin-inline: auto; — two declarations, where a class would have to expose a custom property to be reusable and you would end up writing both anyway. Measured on 17 August 2026: eight consumers out of eight had written those two declarations, every one of them as .wrap, and each had grown a third the argument did not account for — padding-inline, the gutter. The class is utilities/center.css; the custom property the argument feared is what makes it worth having. ON BREAKPOINTS There is no --breakpoint-* token, because CSS cannot use var() inside a media query — @media (min-width: var(--bp-md)) does not work, and shipping a token nobody can use in the one place they want it is worse than shipping none. Prefer container queries, which need no breakpoint at all. Where a viewport media query is unavoidable, the convention is 40rem / 60rem / 80rem, written literally. TARGET SIZE — THE NUMBERS, BECAUSE THE ADVICE YOU HAVE READ IS THREE DIFFERENT NUMBERS They are not in conflict; they are different requirements, and a library has to say which one it meets: 24×24 WCAG 2.2 SC 2.5.8 Target Size (Minimum) — level AA, with a spacing exception: a 24px circle centred on the target may not overlap another target's circle. 44×44 WCAG 2.2 SC 2.5.5 Target Size (Enhanced) — level AAA. Also Apple's HIG figure, in points. 48×48 Material, in dp. So 44 is the touch convention and the AAA bar, not the AA requirement, and a 40px control is conformant at AA. It is still the wrong default for a phone, which is what these two tokens are for. WHY THE FLOOR MOVES WITH THE POINTER A 44px row of controls is right under a thumb and wasteful under a cursor — that is the whole reason design systems sit at 36–40 and then get this review. The platform already models the difference, so nothing here has to guess: the fine-pointer values below are exactly what this library shipped before, and the coarse ones are 44. any-pointer, not pointer. A laptop with a touchscreen reports pointer: fine — its primary input is the trackpad — while any-pointer: coarse matches, and erring towards the larger target is the safe direction of that error. Measured in Chromium, Firefox and WebkKit: all three match any-pointer: coarse under touch emulation. WHAT THE -sm VARIANTS COST, STATED RATHER THAN IMPLIED They do not read these tokens. .btn-sm is 32px, .nav-sm and .segmented-sm are 28px, and that is a deliberate density decision for pointer-first interfaces — a toolbar, a data table's row actions, a settings strip on a desktop. All of them clear the AA floor of 24; none of them reaches 44. If the control is a touch target, do not use the -sm variant, or raise the component's own --*-min-size back to var(--target-size) yourself. Nothing can detect which case you are in.

Container widths, and the floor under anything you tap tokens
LiveTokenValuePurpose
—--container-narrow48rem—
—--container68rem—
—--container-wide80rem—
—--container-full100%—
—--container-prose46remThe column for a page that is read rather than used, and the one number here that is derived rather than chosen: with the border box this library resets to, 46rem minus two --space-4 gutters is 44rem of text, which is the running measure /guidelines/#measure publishes.
—--target-size-min1.5remThe AA floor, and the only one of the three that never moves: 24px is a conformance requirement, not a preference, so a pointer cannot make it smaller.
—--target-size2.5remA control you press: a button, a field, a select.
—--target-size-compact2.25remOne row of a list of them: a menu item, a nav link, a segment, a chip. Smaller under a cursor because it is one of many in a column.

Source: src/css/tokens/sizing.css

Prefer a container query to a breakpoint. There is deliberately no --breakpoint-* token, because var() does not work inside a media query. Where an ancestor can be a container, you do not need one at all — ask how much room the component has rather than how wide the window is:
.main { container: main / inline-size } then @container main (min-inline-size: 60rem).
This page's theme generator switches to two columns that way. It survives minification — the built CSS reads @container main (inline-size>=60rem)— and it keeps working when the component is placed in a narrow column on a wide screen, which a viewport media query gets wrong. Where a viewport query is genuinely unavoidable, the convention is 40rem / 60rem / 80rem, written literally.