Radius scale
A ladder of rungs derived from a single root radius, named for the objects they belong to, plus the nesting rule that keeps concentric corners parallel.
Overview
The radius scale is one number and a set of multipliers. --radius is the root,
and every rung is derived from it. The product theme sets it generously, and
the docs chrome sets it to nothing at all. Change the root and the whole
product moves from square to soft without a single component being edited.
The alternative, and the thing this replaces, is a set of independently chosen radius values. Those drift: somebody rounds a new component to 12px because 8 looked tight, and now there are seven radii in the system and no rule for which to use.
The scale is small on purpose. Six rungs is enough to distinguish a control from a card from a sheet, and few enough that the choice is obvious.
How it works
The ladder. Each rung is the root multiplied by a fixed factor, ascending, and named for the class of object it belongs to rather than for its size:
| Rung | Belongs to |
|---|---|
xs | Inline marks, small indicators, chart elements |
sm | Inputs, chips, small controls |
md | Buttons, list rows, tiles |
lg | Cards, panels, grouped sections |
xl | Sheets, dialogs, full-width surfaces |
full | Pills and circles, which are a different object rather than a bigger rung |
full is deliberately outside the ladder. It is not "the largest radius"; it is a
statement that the element is a capsule, and capsules do not participate in
nesting.
The nesting rule. When one rounded box sits inside another, the inner radius is the outer radius minus the space between them:
inner = outer − paddingIf that comes out at zero or below, the inner element is square. Nothing else is correct: this is the only relationship in which the two curves stay parallel all the way around the corner.
Getting it wrong fails visibly in both directions. Set the inner radius equal to the outer and the gap between them narrows to nothing on the diagonal, which reads as a rendering fault. Set it larger and the inner corner bulges outward towards the parent, which reads as a misaligned element. Neither is subtle once you have seen it.
The rule composes: three levels of nesting subtract twice. It also constrains layout, and usefully so. If the padding is larger than the outer radius, the inner element is square, and if you wanted it rounded you needed less padding or a bigger container.
The docs chrome does not follow this. This site is set in shadcn's lyra
style, whose root radius is zero, so nothing here is rounded and the nesting rule
is trivially satisfied. That divergence is deliberate and explained on
Lyra and the docs chrome. Every
<ComponentPreview> renders under the product theme, which is where the ladder
is actually visible.
Using it
Choose the rung from the object. A button is md because it is a button, not
because md looked right. When two rungs both seem plausible, the object is
probably ambiguous. A "card" that is really a list row wants md.
Compute nested radii from the token, not by eye.
Do
A card at lg with a chart inset by two space steps takes the chart's radius
as lg minus that inset. If the design system's spacing changes, the
relationship still holds because it was expressed as a relationship.
Don’t
A hard-coded 8px on the chart because it looked about right against a 16px
card. It is correct at exactly one padding value, and the next person to
adjust the padding will not know they broke it.
Do not mix full with anything. A pill inside a card does not participate in
the nesting arithmetic. It is a capsule and it stays a capsule. Trying to
subtract from full produces a value that means nothing.
Do not use radius to group. Two elements sharing a radius are not thereby related. Grouping is a job for space and for a container. See Spacing scale.
Overriding the root is a theming decision, not a component one. If a product
wants square corners throughout, it sets --radius to zero and every rung
follows. A component that overrides its own radius has removed itself from that.
Tokens
The root radius, the multipliers, the six rungs and their resolved values are
generated from tokens/shape.json by scripts/build-tokens.mts. Specimens
render from the generated values.
| Token | What it controls | Used by |
|---|---|---|
| --opsin-radius-base0.875rem | The one radius the whole ladder is derived from. | accordion, alert-banner, button, callout, card, care-card, combobox, empty-state, field, menu, metric-tile, popover, questionnaire, radio-group, range-bar, reading-input, result-card, scale-input, segmented-control, select, skeleton, symptom-picker, tab-bar, textarea, timeline-entry, toast, tooltip, trend-sparkline |
| --opsin-radius-none0rem | Full-bleed media and anything that meets a screen edge. | no component |
| --opsin-radius-xs0.25rem | The floor for a nested corner. Tags inside a chip, a swatch inside a legend. | popover, range-legend, surface, symptom-picker, term, tooltip |
| --opsin-radius-sm0.4375rem | Inputs and small controls inside a card. | card, checkbox, combobox, consent-sheet, dialog, divider, field, log-sheet, menu, number-field, progress, reading-input, scroll-area, select, sheet, skeleton, slider, switch, tab-bar, tabs, toast, visually-hidden |
| --opsin-radius-md0.875rem | The default. Buttons, chips, cards. | alert-banner, button, callout, card, combobox, empty-state, menu, metric-tile, popover, questionnaire, radio-group, result-card, scale-input, segmented-control, select, skeleton, symptom-picker, tab-bar, textarea, toast, tooltip |
| --opsin-radius-lg1.3125rem | Cards on a phone, where the card is nearly the width of the screen. | accordion, card, combobox, disclaimer-note, menu, popover, result-card, scroll-area, segmented-control, select, surface, toast |
| --opsin-radius-xl1.75rem | Sheets and dialogs. Applied to the leading edge only when the surface meets a screen edge on the other side. | alert-banner, dialog, sheet |
| --opsin-radius-inner-cardmax(calc(0.875rem - 5 * var(--spacing)), var(--opsin-radius-xs)) | A box inside a card at the comfortable density (card padding p-5). | no component |
| --opsin-radius-inner-card-compactmax(calc(0.875rem - 4 * var(--spacing)), var(--opsin-radius-xs)) | A box inside a card at the compact density (card padding p-4). | no component |
| --opsin-corner-shapesuperellipse(4) | The squircle curvature. Degrades to `round` where corner-shape is unsupported. | alert-banner, callout, card, dialog, metric-tile, result-card, sheet |
| --opsin-border-hairline1px | Every boundary in the system by default. | button, surface |
| --opsin-border-emphasis2px | The boundary of a surface carrying `attention` or `urgent`, where the boundary is one of the three non-colour carriers of the status. | disclaimer-note, field, surface |
| --opsin-border-focus2px | The focus ring. Always 2px with a 2px offset, always in a colour measured against both the surface and the page behind it, and never removed. See /docs/accessibility/keyboard-and-focus. | accordion, body-map, button, card, checkbox, combobox, consent-sheet, link, menu, number-field, popover, questionnaire, radio-group, scale-input, scroll-area, segmented-control, select, slider, switch, symptom-picker, tab-bar, table, tabs, term, textarea, toast, tooltip |
| --opsin-border-focus-offset2px | Offset for the focus boundary. | accordion, body-map, button, card, checkbox, combobox, consent-sheet, link, menu, number-field, popover, questionnaire, radio-group, scale-input, scroll-area, segmented-control, select, slider, switch, symptom-picker, tab-bar, table, tabs, term, textarea, toast, tooltip |
Accessibility impact
- Radius reduces the usable hit area at the corners. A 44pt control at the
mdrung has slightly less than 44pt of tappable area on its diagonals. The floor on Density and touch is measured accordingly, and it is the reason small controls do not take large rungs. - Focus rings follow the radius and can be clipped by a parent's. An element
at
mdinside a container atsmwith no padding will have its outline trimmed at the corners. The nesting rule prevents this as a side effect; drawing the ring outside the box with an offset prevents it properly. - A larger radius on a small element can hide a boundary. Where a control's edge is the only thing distinguishing it from its background, a heavy radius plus a low-contrast border can make it hard to see it is a control at all. That is a contrast problem, measured on Contrast conformance, but the radius is what makes it easy to reach.
- Nothing here is announced. Radius carries no information to assistive technology, which is why it may not carry information at all.
Related
- Squircle says what curve these radii are actually drawn with,
and what happens where
corner-shapeis unsupported. - Spacing scale is the other half of the nesting arithmetic.
- Shape tokens has the generated ladder with resolved values.
Shape
Why the corners in a health product are superellipses rather than circular arcs, what shape is allowed to mean, and the one thing it must never carry.
Squircle
The corner-shape property, the superellipse parameter opsinjs ships, and why this is the safest progressive enhancement in the whole system.