Shape & radius
24px radius. 24px inset. That pairing is the signature of the system, and everything else derives from it.
The scale
Radii are 8px multiples, because the nesting formula requires them to be (below).
--radius-none: 0px /* rule lines, full-bleed dividers, table cells */
--radius-xs: 4px /* badges, tags, checkbox */
--radius-sm: 8px /* inputs, small buttons, list-row hover */
--radius-md: 12px /* medium controls, inner panels */
--radius-lg: 16px /* nested cards, sheets on mobile */
--radius-xl: 24px /* CARD DEFAULT · dialogs · panels */
--radius-2xl: 32px /* hero surfaces, marketing */
--radius-full: 9999px /* avatars, pills, presence dots */
--radius-xl: 24px is the base. Every other radius in a composition should be derived from it, not chosen.
The nesting formula
r_inner = r_outer − gap
Where gap is the distance between the outer border and the inner element's border. This is not a stylistic preference — it is the geometry that makes two concentric rounded rectangles concentric. Get it wrong and the corners visibly diverge, which reads as sloppy even to people who can't name why.
Worked examples
| Outer | Gap (inset) | Inner radius | Where |
|---|---|---|---|
| 24 | 24 | 0 | An element flush against the full card inset is square. Correct and intentional — this is why full-bleed table rows and dividers inside a card have no radius. |
| 24 | 8 | 16 | A nested panel inset 8px from the card edge |
| 24 | 12 | 12 | An inner card at 12px inset |
| 24 | 16 | 8 | An input inset 16px |
| 16 | 8 | 8 | A row inside a nested card |
| 32 | 8 | 24 | A card inside a hero surface |
The consequence people miss
24 outer + 24 inset = 0 inner. An element that fills the entire content box of a 24px-radius card must have square corners. This is why:
- Table rows inside a card are square, and their hover background is square with the card's corner clipped by
overflow-hiddenon the card. - A full-bleed chart at the bottom of a card inherits the card's bottom corners via
overflow-hidden, not its own radius. - A list-row hover background that is inset 8px from the card edge gets
radius-lg(16), notradius-sm.
When you can't satisfy the formula
If the required inner radius comes out negative, the inner element must be square (0) and you should question the composition — it usually means the inner element should be full-bleed rather than inset.
Squircle vs. circular corners
At 24px, the difference between a true circular arc and a superellipse (squircle) is visible. Apple uses continuous curvature; CSS border-radius does not.
Decision: circular arcs. We ship standard border-radius.
Rationale: squircles require either an SVG mask, corner-shape: squircle (Chrome 139+, not yet baseline as of this writing), or a paint() worklet. Each breaks something — mask kills subpixel text antialiasing on the clipped edge, the worklet doesn't run in Safari, and corner-shape isn't universally available. A perfect circular corner beats a broken squircle.
Revisit gate: when corner-shape: squircle reaches Baseline Widely Available, adopt it progressively:
.card { border-radius: 24px; }
@supports (corner-shape: squircle) {
.card { corner-shape: squircle; }
}
No layout changes required. Add it to roadmap.md as a deferred item.
Radius by component class
| Class | Radius | Note |
|---|---|---|
| Card, panel, dialog | 24 | The default |
| Sheet / drawer (mobile) | 16 top corners only | Bottom corners are off-screen; rounding them costs nothing and gains nothing |
| Popover, dropdown, menu | 12 | Smaller because they're smaller — a 24px radius on a 160px-wide menu eats the corners |
| Tooltip | 8 | |
| Input, select, textarea | 8 | |
| Button (md, 40px) | 8 | |
| Button (lg, 48px) | 12 | Scales with height |
| Button (sm/xs) | 8 / 4 | |
| Pill / badge / status | full | |
| Avatar | full | Except bank and merchant logos, which are 8 — see imagery |
| Payment card visual | 5.9% of width | ISO 7810 physical spec, ~16px at 272px wide |
| Table row hover | 0 (full-bleed) or 8 (inset) | Per the nesting formula |
| Chart bar | 4 top corners | Only when bar width > 12px; below that, square |
| Skeleton | matches the element it replaces | A rounded skeleton for a square element is a tell |
Corner logic
Rules that prevent the most common shape mistakes.
Corners that touch a viewport edge are square. A sheet at the bottom of the screen, a sidebar flush left, a full-width mobile card — round only the corners that are actually visible. Rounding an off-screen corner is a wasted 24px of content width.
Adjacent segmented controls share a radius envelope. In a button group, the first child rounds left, the last rounds right, the middles are square. Never round every child.
Clip, don't double-round. When content must follow a card's corner (an image, a chart, a colored bar), put
overflow: hiddenon the card and give the childradius: 0. Two separately-rounded elements at the same corner never align perfectly.overflow: hiddenhas a cost. It creates a new stacking/clipping context, so a shadow, focus ring, or tooltip inside a clipped card will be cut off. If you need both clipping and an escaping child, clip a dedicated inner wrapper, not the card.Radius does not scale with density. A
compacttable still lives in a 24px card. Density changes spacing and type, not shape — otherwise the product's identity changes when a user flips a setting.Focus rings sit outside the radius.
outline: 2px solid var(--focus); outline-offset: 2px;followsborder-radiusautomatically and never gets clipped. Never build a focus ring withbox-shadowon a clipped element.
Borders and shape
Radius and border are one system. A 1px border on a 24px radius must be drawn on the shape, not inside it. Practical consequences:
- Use
border(which follows radius) overbox-shadow: 0 0 0 1px(which also follows radius but stacks differently with other shadows). Preferborderfor resting state,box-shadowring for transient/focus states. - On a nested element,
borderadds 1px to the outside; the nesting formula uses the border box. Two 24px cards separated by an 8px gap where both have borders: inner radius is still24 − 8 = 16.
Full elevation treatment in elevation.