opsinjs
FoundationsToken familiesShape

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:

RungBelongs to
xsInline marks, small indicators, chart elements
smInputs, chips, small controls
mdButtons, list rows, tiles
lgCards, panels, grouped sections
xlSheets, dialogs, full-width surfaces
fullPills 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 − padding

If 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.

TokenWhat it controlsUsed by
--opsin-radius-base0.875remThe 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-none0remFull-bleed media and anything that meets a screen edge.no component
--opsin-radius-xs0.25remThe 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.4375remInputs 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.875remThe 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.3125remCards 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.75remSheets 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-hairline1pxEvery boundary in the system by default.button, surface
--opsin-border-emphasis2pxThe 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-focus2pxThe 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-offset2pxOffset 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 md rung 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 md inside a container at sm with 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.
  • Squircle says what curve these radii are actually drawn with, and what happens where corner-shape is unsupported.
  • Spacing scale is the other half of the nesting arithmetic.
  • Shape tokens has the generated ladder with resolved values.

On this page