Shape & radius
24px radius, 24px inset. That pairing defines the system's shape. Every other radius derives from it.
The scale#
Radii sit on a 4px grid because the nesting formula (below) needs them to be.
--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. Derive every other radius in a composition from it. Don't pick one.
The radius scale
The nesting formula#
r_inner = r_outer − gap
gap is the distance between the outer border and the inner element's border. This is geometry, not taste: it's what keeps two rounded rectangles concentric. Get it wrong and the corners visibly drift apart, which looks sloppy even to people who can't say why.
Two corners, one center
Worked examples#
| Outer | Gap (inset) | Inner radius | Where |
|---|---|---|---|
| 24 | 24 | 0 | An element flush against the full card inset is square. That's intentional, and it's why full-bleed table rows and dividers in 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 whole content box of a 24px-radius card has square corners. So:
- Table rows in a card are square. Their hover background is square too, and
overflow-hiddenon the card clips it to the card's corner. - 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.
Square inside round
When you can't satisfy the formula#
If the inner radius comes out negative, make the inner element square (0) and rethink the composition. Usually the inner element should be full-bleed, not inset.
Squircle vs. circular corners#
At 24px you can see the difference between a circular arc and a superellipse (squircle). Apple uses continuous curvature. CSS border-radius doesn't.
Decision: circular arcs. We ship standard border-radius.
Squircles need an SVG mask, corner-shape: squircle (Chrome 139+, not yet Baseline as of this writing), or a paint() worklet. Each one breaks something. The mask kills subpixel text antialiasing on the clipped edge, the worklet doesn't run in Safari, and corner-shape isn't available everywhere. A clean circular corner beats a broken squircle.
Revisit when corner-shape: squircle reaches Baseline Widely Available, and adopt it progressively:
.card { border-radius: 24px; }
@supports (corner-shape: squircle) {
.card { corner-shape: squircle; }
}
No layout changes needed. 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, so rounding them does nothing |
| Popover, dropdown, menu | 12 | Smaller surface, smaller radius. 24px 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 for the most common shape mistakes.
Corners that touch a viewport edge are square. For a sheet at the bottom of the screen, a sidebar flush left or a full-width mobile card, round only the visible corners. Rounding an off-screen corner wastes 24px of content width.
Adjacent segmented controls share a radius envelope. In a button group, the first child rounds left, the last rounds right, and the middle ones are square. Never round every child.
Clip, don't double-round. When content has to follow a card's corner (an image, a chart, a colored bar), put
overflow: hiddenon the card and give the childradius: 0. Two elements rounded separately at the same corner never line up.overflow: hiddenhas a cost. It creates a new clipping context, so a shadow, focus ring or tooltip inside the card gets cut off. If you need clipping and a child that escapes, clip an inner wrapper instead of the card.Radius doesn't scale with density. A
compacttable still sits in a 24px card. Density changes spacing and type, not shape. Otherwise the product would look like a different product 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 is drawn on the shape, not inside it. In practice:
- Both
borderandbox-shadow: 0 0 0 1pxfollow radius, but the shadow stacks differently with other shadows. Useborderfor the resting state and abox-shadowring for transient and focus states. - On a nested element,
borderadds 1px to the outside, and the nesting formula uses the border box. For two bordered 24px cards with an 8px gap, the inner radius is still24 − 8 = 16.
Elevation is covered in elevation.