Skip to content

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-hidden on 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), not radius-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.

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

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

  3. Clip, don't double-round. When content must follow a card's corner (an image, a chart, a colored bar), put overflow: hidden on the card and give the child radius: 0. Two separately-rounded elements at the same corner never align perfectly.

  4. overflow: hidden has 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.

  5. Radius does not scale with density. A compact table 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.

  6. Focus rings sit outside the radius. outline: 2px solid var(--focus); outline-offset: 2px; follows border-radius automatically and never gets clipped. Never build a focus ring with box-shadow on 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) over box-shadow: 0 0 0 1px (which also follows radius but stacks differently with other shadows). Prefer border for resting state, box-shadow ring for transient/focus states.
  • On a nested element, border adds 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 still 24 − 8 = 16.

Full elevation treatment in elevation.

Told when this spec changes

The docs are the contract the components are built against. When one changes, the component changes with it — and the copy in your repo does not. One email when that happens.

One address, no list rental, unsubscribe in one click.

Works in your QuickBooks Online or Xero file

Get your books closed every month

Lopifi is building AI bookkeeping that keeps your books closed on US GAAP as transactions happen, with a CPA review every quarter, for $200 a month. We’re not open yet.

$200/month when we open. No card now.