Foundations
Space & shape
The page gutter, the measures, and the five radii.
Measures are capped in ch rather than px, so they track the type they are set in. --measure-record is a ceiling on a listed record’s description, not a width: a narrower column still wins.
One ladder, and one factor that moves the whole of it: every step is a multiple of --radius-factor, so a theme sets one number and the steps keep their proportions. That is what makes nesting safe — two rounded edges separated by a gap of p are concentric only when the inner radius is the outer minus p, and --radius-row and --radius-frame name both directions. A 50% circle is geometry rather than a corner and is not on the ladder at all.
Every component is written in logical properties — ps- and pe- rather than pl- and pr-, start- and end- rather than left- and right- — so a right-to-left document mirrors without a stylesheet of its own. A test fails the build on a physical one, because retrofitting direction into forty components after the fact is a sweep nobody schedules and a failure nobody sees. Flip any example above to RTL.
Layout
| Sample | Token | Value | Notes |
|---|---|---|---|
--maxw | 1288px | — | |
--col | 8.3333% | — | |
--row | 52px | — | |
--pad | clamp(18px, calc(4.4 * var(--fluid)), 64px) | — | |
--sec | clamp(54px, calc(7 * var(--fluid)), 96px) | — | |
--sec-tight | clamp(40px, calc(5 * var(--fluid)), 68px) | — | |
--bar-h | calc(var(--control-h-md) + 0.75rem) | — | |
--secondary-text | var(--ink-3-aa) | — | |
--page-pad | var(--pad) | ─── Layout ───────────────────────────────────────────────────────────── | |
--w-page | var(--maxw) | — | |
--w-reading | 46rem | Caps the prose track only, which is what a 73ch measure is for. | |
--measure-record | 66ch | The cap on a listed record's description — a row, a card, a CV entry. In `ch` rather than px so it tracks the type it is set in, and one token rather than the six hand-picked widths surfaces had drifted into. A ceiling, not a width: a narrower column still wins. | |
--scroll-offset | calc(var(--bar-h) + 1.5rem) | How far below the viewport top an anchored heading comes to rest — a fixed masthead plus a line of air. Both halves are now named: the masthead is `--bar-h`, so a bar that moves takes its anchors with it, where `88px` was a number somebody derived once by hand and nothing kept in step. |
Radius
| Sample | Token | Value | Notes |
|---|---|---|---|
--radius-factor | 1 | — | |
--radius-gate | min(1, var(--radius-factor)) | Clamps the factor at 1 for the ADDING direction only. A frame's air is a fixed number of pixels, so `--radius-lg + 16px` would leave a rounded frame around a square panel the moment the theme went square; gated, it collapses to nothing along with everything else. | |
--radius-xs | calc(4px * var(--radius-factor)) | a mark inside a tight box | |
--radius-sm | calc(6px * var(--radius-factor)) | chips, keys, list rows | |
--radius | calc(8px * var(--radius-factor)) | inputs, code, small plates | |
--radius-lg | calc(12px * var(--radius-factor)) | cards, dialogs, menu panels | |
--radius-pill | calc(999px * var(--radius-factor)) | capsules and counters | |
--radius-row | max(0px, calc(var(--radius-lg) - 0.375rem)) | a row in a 6px-padded panel | |
--radius-frame | calc(var(--radius-lg) + 1rem * var(--radius-gate)) | a frame 16px outside a panel |
Density
The second theming axis, and the only other one. Set data-density="compact" on any container and every control below it tightens — nothing has to be told twice. At the default, a medium control is 44px, the pointer target WCAG 2.5.5 asks for; compact drops it to 36px, which still clears 2.5.8 with room and no longer meets 2.5.5. It is for a dense desktop tool driven by a mouse, and it is a real trade rather than a free one. Flip it on any example above to watch.
| Sample | Token | Value | Notes |
|---|---|---|---|
--control-h-sm | 2.25rem | 36px | |
--control-h-md | 2.75rem | 44px — WCAG 2.5.5 | |
--control-h-lg | 3rem | 48px | |
--control-px-sm | 1rem | — | |
--control-px-md | 1.5rem | — | |
--control-px-lg | 1.75rem | — | |
--control-py-sm | 0.5rem | The vertical padding is part of the axis too, and has to be: a control whose padding alone exceeds the compact height never shrinks, because `min-height` is a floor and not a ceiling. | |
--control-py-md | 0.75rem | — | |
--control-py-lg | 0.75rem | — | |
--control-lh | 1.2 | And so is the LEADING, which is the term that guard was missing. `min-height` is a floor, and what has to stay under it is the whole box: the line, the padding and the border. The padding was scaled and the line was left inheriting the body's 1.6 — so `sm` measured 20.8 + 16 + 2 = 38.8 against a 36px token, `md` 46 against 44 and `lg` 50 against 48. Every text button in the system was two to three pixels taller than the number documenting it, and the floor never bound because the box had already cleared it. One value on both densities: the font sizes do not move with density, only the padding and the heights do, so this does not have to. It is not a reading leading — a control's label is one line, and 1.2 leaves every size at every density inside its own height with room to spare. The test in `tokens.test.ts` is what keeps that true. | |
--control-gap | 0.625rem | — | |
--field-px | 0.875rem | Text inputs are measured separately: they are read as much as they are hit, so their padding tracks the type rather than the target. | |
--field-py | 0.625rem | — |
Icons
| Sample | Token | Value | Notes |
|---|---|---|---|
--ico-s | 14px | ─── Icons ─── three sizes, and no weight: a Remix Icon glyph is a filled path. | |
--ico-m | 16px | — | |
--ico-l | 20px | — |
Stacking order
| Sample | Token | Value | Notes |
|---|---|---|---|
--z-rule | 1 | ─── Stacking ─── seven ranks, named for what sits at each. Every overlay in the package portals to document.body, so all of them are siblings in the ROOT stacking context and the rank is the whole of the decision — there is no ancestor left to nest one inside another. Read it as a sentence: a sticky header clears the page, a drawer clears the header, a scrim covers both, a modal sits on its scrim, an ANCHORED panel clears the modal it was opened from, and a toast clears everything because it is the only rank that has to survive a modal asking a question. --z-anchored is the one that is not obvious, and it is the one that was wrong. A popover, a menu or a select listbox is opened FROM something, and the surface it is most often opened from is a modal — a select inside a modal form is one of the most ordinary shapes in an application. Ranked below the modal, as it was, the panel is painted behind the very dialog that summoned it. The rank it replaced, --z-palette, was read by nothing: a command palette is a Dialog, so it lands at --z-modal, and its order against a second modal is settled by document order — which is the right answer, because it moves a scrim and its panel together. | |
--z-sticky | 10 | — | |
--z-drawer | 100 | — | |
--z-scrim | 200 | — | |
--z-modal | 210 | — | |
--z-anchored | 220 | — | |
--z-toast | 300 | — | |
--z-dropdown | var(--z-anchored) | ─── Stacking ─── two component-facing names onto the seven ranks. ────── The other five ranks are read by their own names; only these two had a component-facing spelling that outlived the rank it pointed at. --z-dropdown pointed at --z-drawer, 100, which put every anchored panel under the modal at 210 that opened it. | |
--z-overlay | var(--z-scrim) | — |