Radius
Overview
Radius sets how round the system's corners are. Every value comes from the
master numeric scale, but Radius ships one semantic knob — --radius — plus four
sizes derived from it. Turn the knob and the whole UI rounds together.
Why a token here when Spacing shipped none? Because shadcn components read --radius
directly (rounded-lg → var(--radius)). The token isn't indirection — it's the interface the
components expect, and the single control that keeps every corner in step.
Source of truth: tokens/semantic.css in the Monogem codebase.
Principles
- One knob rounds the system. Every size is derived from
--radius; change that one value and sm/md/lg/xl all move with it. - Sourced from the scale. The base is
--scale-2(8px), so at the default every size lands on a scale step — nothing drifts off the shared ruler. - Size-named, intent in the recipe. Tokens are named by size (like the shadow ramp); the "use
for" column carries the meaning (
--radius-md= button roundness). fullis the one exception. A single off-scale value (9999px) for pills and avatars.
Tokens
| Token | Formula | px (default) | Scale step | Use for |
|---|---|---|---|---|
--radius-sm | --radius − 4px | 4 | --scale-1 | Tags, inputs |
--radius-md | --radius − 2px | 6 | --scale-1-5 | Buttons |
--radius-lg | --radius | 8 | --scale-2 | Cards, popovers (default) |
--radius-xl | --radius + 4px | 12 | --scale-3 | Modals, large surfaces |
--radius-full | 9999px | — | off-scale | Pills, avatars, badges, circular |
--radius itself is the base knob: --radius: var(--scale-2) (8px). In Tailwind these map to
rounded-sm / rounded-md / rounded-lg / rounded-xl / rounded-full — tokens and utilities
never disagree.
The single knob. Every size is
calc()-derived from--radius. Re-point--radiusonce — in:rootor inside a media query — and the whole set scales together. No per-size edits.
Rationale
Why ship a token when Spacing didn't? Spacing shipped zero because Tailwind already maps
p-4/gap-4 onto scale steps, so a --space-md alias would be pure indirection. Radius is the
reverse: shadcn/ui components consume --radius directly, so the token is the real interface —
and it adds a single knob spacing never needed.
Why an 8px base (not shadcn's 10px)? With the base at 8px, the standard shadcn derivations land on exact scale steps — 4 / 6 / 8 / 12. A 10px base would push two sizes (10, 14) off our scale. 8px keeps the whole set on the shared ruler.
Why calc() instead of pointing each size at a scale token? The calc() form is what makes
--radius a real knob — change it and every size follows, exactly as shadcn components expect.
Pointing each size at its own scale token would read cleanly but break that single control.
Why is full 9999px, not 50%? 50% turns a rectangle into an ellipse; a large fixed radius
clamps to a clean pill on any shape and a circle on squares.
What about responsive corners? Radius rarely changes across breakpoints, so it ships no
breakpoint tokens (that's Grids / Layout's job). If a real need appears, the single knob covers it
in one line: @media (min-width: 1024px){ :root{ --radius: var(--scale-3); } }.
Do / Don’t
Do
- Use a
--radius-*token for every corner; pick the size by task from the table. - Match the size to the component: inputs
sm, buttonsmd, cardslg, modalsxl, pillsfull. - Re-point
--radius(once) to re-round the whole system — don't edit sizes individually.
Don’t
- Hard-code corner values (
6px,10px) — reach for a token. - Add a
--radius-none— a square corner is--scale-0/rounded-none. - Put
--radius-fullon a rectangle expecting a circle — it makes a pill; circles need a square.