@arag 0.1.0

Radius, border & shadow

Twenty-four tokens covering corner radius, border width, the focus ring composite, and a six-step elevation set built from two derived tints. Every value here is a literal in tokens.css and nowhere else.

Radius

Eight steps. --radius-none is a real token rather than a bare 0, and --radius-full is a large pixel value rather than 50% — a percentage radius on a non-square box gives you an ellipse, which is almost never what a pill button wanted.

The boxes below are identical in every respect except the corner.

--radius-none
--radius-xs
--radius-sm
--radius-md
--radius-lg
--radius-xl
--radius-2xl
--radius-full
Where each step is used in base.css.
TokenValueTypical use
--radius-noneSquaring off something that inherited a radius — a code block flush inside an example.
--radius-xsHairline softening. Bars, ticks, tiny indicators.
--radius-smInline code, badges, list items in a nav.
--radius-mdThe default control radius. Inputs, selects, buttons.
--radius-lgCards, panels, dialogs.
--radius-xlLarge surfaces, kiosk-scale tiles.
--radius-2xlFull-bleed hero and marketing blocks.
--radius-fullPills, avatars, the theme switch in this page's masthead.

Border width

Four widths, in px rather than rem. A border is a hairline, not type: scaling it with the root font size makes a 1px rule land on a fractional device pixel and go grey.

--border-width-0
--border-width-1
--border-width-2
--border-width-4

There is no --border-width-3. The numbers are the pixel values, not scale positions, so the gap is information rather than an omission. --border-width-0 exists for the same reason --space-0 does: so that removing a border is still written as a token.

Border colour is not border width

Naming

Widths are --border-width-0 through --border-width-4. Colours are --color-border, --color-border-subtle and --color-border-strong. There is no --border-color-* and there never will be.

The split is by what the value is, not by what it is used for. Everything that resolves to a colour lives in the colour family and is documented on the colour page, because colour tokens theme and lengths do not. A border is the one place those two families meet in a single shorthand, which is exactly why they are kept on separate pages: reading --border-width-1 solid var(--color-border) should make it obvious that only the second half changes between light and dark.

The focus ring

Three tokens compose one ring. base.css applies them as an outline, not a box-shadow and not a border, so the ring never changes layout and never gets clipped by a parent's overflow.

TokenValueAliases
--focus-ring-widthvar(--border-width-2)
--focus-ring-offsetvar(--border-width-2)
--focus-ring-colorvar(--color-focus), itself var(--color-brand)

Press Tab to move into these. Clicking a button will not show the ring — that is :focus-visible doing its job, not a bug.

A link, also focusable
CSS from base.css
:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
}

The colour is aliased twice on purpose: --focus-ring-color--color-focus--color-brand. Retune --hue-brand and the focus ring follows without anyone touching this file. The middle link exists so a product that needs a focus colour distinct from its brand — a high-contrast kiosk build, for instance — has one place to break the chain, rather than having to override every ring at every call site.

Do not

Do not remove the ring and put nothing back. If a component needs a different focus treatment, redefine --focus-ring-color or --focus-ring-width on that component and let the same outline rule draw it. The offset is a real token so the ring can sit clear of a filled control without a second element.

Shadow

Six elevations plus --shadow-inset, built from exactly two tint tokens.

Two tints, derived not authored

Every elevation is two layered shadows: a tight, low-offset penumbra that reads as contact, and a broader, softer umbra that reads as distance. That is why --shadow-md looks like an object above a surface rather than a grey smudge behind it — one shadow alone cannot do both jobs at once.

TintResolvedRole
--shadow-penumbra Tight contact shadow. Lighter tint, small blur, no spread.
--shadow-umbra Broad ambient shadow. Denser tint, large blur, negative spread.

Neither tint is hand-authored. Both are color-mix() results: a step from the neutral ramp mixed against transparent to get the alpha, wrapped in light-dark() so the dark theme can be much denser than the light one. The mix result is still a <color>, which is all light-dark() will accept.

--shadow-penumbra: light-dark(
  color-mix(in oklab, var(--color-neutral-900) 6%, transparent),
  color-mix(in oklab, var(--color-black) 30%, transparent)
);

This is the point of the whole arrangement. These two were previously the only hand-authored colours in tokens.css, which meant retuning --hue-neutral moved every grey in the system while the shadows stayed on the old hue. Deriving them from the ramp keeps a shadow the same colour family as the greys it is a shadow of, automatically.

The elevation set

TokenLayersUse
--shadow-nonenoneFlat. A token, so “no elevation” is still declarative.
--shadow-xspenumbra onlyA pressed segment, a kbd key. Barely off the page.
--shadow-smpenumbra + umbraResting cards and list rows.
--shadow-mdpenumbra + umbraRaised cards, hovered rows.
--shadow-lgpenumbra + umbraDropdowns, popovers, sticky bars.
--shadow-xlpenumbra + umbraModal dialogs. base.css puts this on dialog.
--shadow-insetinset umbraWells and sunken tracks. Reads as recessed, not raised.

Light and dark, side by side

Shadows read completely differently across themes, and this is the section that needs the two-panel treatment more than any other on the site. In light mode the elevation ladder is obvious. In dark mode it is nearly invisible: a dark shadow on a dark surface has almost nothing to contrast against, so the steps collapse into each other.

Гэрэлтэй
--shadow-none
--shadow-xs
--shadow-sm
--shadow-md
--shadow-lg
--shadow-xl
--shadow-inset
Харанхуй
--shadow-none
--shadow-xs
--shadow-sm
--shadow-md
--shadow-lg
--shadow-xl
--shadow-inset

Those two panels are one page in one theme. The resolved values printed under each name are read per panel at runtime, which is why the offsets match and the tints do not.

Pair a raised surface with a shadow

The semantic surface roles are deliberately asymmetric, and the shadow tokens are the other half of that decision. See the elevation-asymmetry note on theming & dark mode for the full reasoning.

Theme--color-surface--color-surface-raisedWhat carries elevation
Гэрэлтэй --color-white --color-white — identical The shadow, entirely. Colour contributes nothing.
Харанхуй --color-neutral-900 --color-neutral-800 — one ramp step lighter Lightness, mostly. The shadow is nearly invisible.
Consequence

Pair a raised surface with a shadow token. Never rely on colour alone.

A card that sets only background-color: var(--color-surface-raised) is completely invisible in light mode, because raised and base are the same white. A card that sets only box-shadow: var(--shadow-md) all but disappears in dark mode. Set both, every time, and each theme is carried by whichever half actually works there.

.card {
  background-color: var(--color-surface-raised);
  box-shadow: var(--shadow-md);
  border-radius: var(--radius-lg);
}