Foundations
Shape
Five steps from one number, and the arithmetic that keeps two of them concentric.
Five steps, and one number moves all of them. 4px, 6px, 8px, 12px and 999px, each written as calc(N * var(--radius-factor)) rather than as a literal — so a theme sets the factor and the steps keep their ratios instead of drifting out of proportion the way four independently re-typed numbers always do. data-radius="sharp" sets the factor to 0 and the whole system squares off; data-radius="round" sets it to 2 and every step doubles. Both values live in themes.css, which is a separate CSS entry point.
Each step names what it is for, and the assignments are not interchangeable. --radius-xs, 4px, is a mark inside a tight box. --radius-sm, 6px, is a chip, a key, a list row. --radius, 8px, is the CONTROL step — a button, an input, a select trigger and a textarea all draw the same corner, and that agreement is recent: the button used to be a capsule, which beside an 8px field is two different ideas of what a control is, and it was the one inconsistency in the system every reader noticed and no component page could explain. --radius-lg, 12px, is a card, a dialog, a menu panel. --radius-pill is the capsules and the counters — a badge, a status pill, a segmented strip, a progress track.
A true circle is not on the ladder and never squares off with the theme. An avatar, a status dot, a spinner and a radio are rounded-full, because a radio is round so that it cannot be mistaken for a checkbox — that is geometry carrying meaning, not a corner treatment.
THE NESTING LAW. Two rounded edges separated by a gap of p are concentric only when the inner radius is the outer minus p. Anything else pinches: the gap narrows as it turns, and that is the mismatch you see a moment before you can name it. Both directions are named so no surface has to guess. --radius-row subtracts — max(0px, calc(var(--radius-lg) - 0.375rem)), which is 12px minus the 6px a panel pads its rows by, so 6px. --radius-frame adds — calc(var(--radius-lg) + 1rem * var(--radius-gate)), so 28px for a frame sitting 16px outside a 12px panel.
--radius-gate is min(1, var(--radius-factor)), and it guards the adding direction only. A frame’s air is a fixed number of pixels, so an ungated --radius-lg + 16px would leave a rounded frame around a perfectly square panel the moment the theme went sharp; gated, the frame collapses with everything else. The subtracting direction needs no gate because max(0px, …) already floors it. Do the arithmetic once at factor 1 and it holds at 0 and at 2, which is the only reason a theming axis this blunt is safe to ship.
Read nextSpace & shapeElevation
Radius
Read out of the package CSS at build time. The authority on which step goes where is the prose above — xs a mark inside a tight box, sm a chip or a list row, --radius a control, lg a card or a panel, pill a capsule or a counter.
| 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 |