---
title: "Radius scale"
description: "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."
url: "https://opsinjs.pensievelabs.org/foundations/shape/radius-scale"
source: "https://opsinjs.pensievelabs.org/foundations/shape/radius-scale.md"
section: "Foundations"
kind: "foundation"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["border radius", "radius ladder", "corner radius", "concentric corners"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="foundation" />

## Overview [#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 [#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:

```text
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.

<RadiusSpecimen step="lg" />

**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](../../theming/lyra-and-the-docs-chrome.mdx). Every
`<ComponentPreview>` renders under the product theme, which is where the ladder
is actually visible.

## Using it [#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.**

<DoDont>
  <DoDont.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.
  </DoDont.Do>

  <DoDont.Dont>
    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.
  </DoDont.Dont>
</DoDont>

**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](../space/spacing-scale.mdx).

**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 [#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.

<TokenTable scope="shape" />

## Accessibility impact [#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](../space/density-and-touch.mdx) 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](../../accessibility/contrast-conformance.mdx), 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 [#related]

* [Squircle](./squircle.mdx) says what curve these radii are actually drawn with,
  and what happens where `corner-shape` is unsupported.
* [Spacing scale](../space/spacing-scale.mdx) is the other half of the nesting
  arithmetic.
* [Shape tokens](./tokens.mdx) has the generated ladder with resolved values.
