Radius
Seven steps and one nesting rule. Corner radius is a shape language: it tells the user what a thing is before they read it.
Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.
--radius-xs4pxCheckbox, tag, code chip, focus ring on inline text.--radius-sm6pxMenu item, extra-small button, nested item inside an 8px parent.--radius-md8pxButtons, inputs, selects. The interactive default.--radius-lg12pxPopovers, alerts, large buttons, nested cards.--radius-xl16pxCards, panels, preview surfaces. The container default.--radius-2xl20pxDialogs, command palette, hero surfaces.--radius-3xl28pxBottom sheets and anything anchored to a screen edge.full∞Pills, avatars, badges, switches — shapes that are never pressed like a button.The nesting rule
Inner radius equals outer radius minus the padding between them. Get it wrong and the corners pinch — visible even to people who cannot name what is bothering them.
Shape as category
Three radii, three meanings. The user never reads a legend — they infer the rule from consistent exposure.
Edge anchoring
A surface attached to a viewport edge rounds only the corners that are not touching it. Rounding all four makes it look detached and floating.
Interaction states
Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.
Every part, every measurement, and the reason it is that number.
Four levels of nesting: 20 → 16 → 8 → 4, with 16px, 12px and 8px of padding between them. Every inner radius is its parent minus the padding.
- Outermost20px · --radius-2xl
Dialog level. The largest radius that still reads as a rectangle rather than a lozenge at typical dialog widths.
- First child16px = 20 − 4
With 4px of padding the curves stay parallel. Any larger and the child’s corner starts crowding the parent’s.
- Second child8px = 16 − 8
Padding here is 12px, so strictly the child could be 4px. 8px is within tolerance — the rule is a floor, not a formula to two decimal places.
- Innermost4px · --radius-xs
The floor of the scale. Below 4px a corner reads as square, so there is no reason to have a 2px step.
- Border alignment1px inside the radius
Borders are drawn inside the radius, so a bordered element’s visible corner is 1px tighter than its declared radius. Ignore it below 12px; compensate above.
Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.
Radius
| Token | Value | Used for |
|---|---|---|
| --radius-xs | Checkboxes, code chips, inline focus rings | |
| --radius-sm | Menu items, xs buttons, nested rows | |
| --radius-md | Buttons, inputs, selects | |
| --radius-lg | Popovers, alerts, lg buttons | |
| --radius-xl | Cards and panels | |
| --radius-2xl | Dialogs and the command palette | |
| --radius-3xl | Bottom sheets | |
| rounded-full | Badges, chips, avatars, switches |
Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.
| Size | Radius | When to use |
|---|---|---|
| Extra small | 4px | Elements under 20px tall: checkboxes, tags, keyboard hints. |
| Small | 6px | Elements 24–28px tall, and anything nested inside an 8px parent. |
| Medium | 8px | Elements 32–40px tall. Every standard control. |
| Large | 12px | Elements 44px+ tall, and floating surfaces under ~320px wide. |
| Extra large | 16px | Content containers of any size. |
| 2XL | 20px | Modal surfaces 400px+ wide. |
| 3XL | 28px | Edge-anchored sheets. Applied to two corners only. |
| Full | 9999px | Any element whose height defines its shape: pills, dots, avatars. |
Not a checklist to run at the end. These are the requirements the component was built from.
Contrast
- Radius does not affect contrast, but it does affect how much of a border is visible. At 28px, roughly 12% of the perimeter is curve — a low-contrast border becomes harder to trace.
- A focus ring follows the element’s radius. On a fully rounded element the ring is a circle, which needs 3:1 against both the element and the page just like any other.
Keyboard
| Tab | Focus rings inherit border-radius automatically via outline, so no per-shape work is needed. |
Screen readers
- Radius is purely visual and is never announced. It must never be the only way a category is communicated.
Focus & touch
- outline follows border-radius in every modern browser, which is one more reason to use outline for focus rather than a box-shadow ring.
- A large radius reduces the effective corner hit area slightly, because the corners are outside the shape. It is negligible below 20px; on a 28px sheet handle, keep interactive targets away from the corners.
| Attribute | Applied to | Notes |
|---|---|---|
| forced-colors: active | All surfaces | High Contrast Mode keeps the radius but replaces colours. Shape becomes the primary categorical signal, which is an argument for keeping the language consistent. |
Example usage
1// Always reference the token, never a literal2<Card className="rounded-[var(--radius-xl)]" />3<Button className="rounded-[var(--radius-md)]" />4<Badge className="rounded-full" />56// Nesting: derive the inner radius from the outer minus padding7const OUTER = 168const PAD = 89<div style={{ borderRadius: OUTER, padding: PAD }}>10 <div style={{ borderRadius: OUTER - PAD }} />11</div>1213// Or in CSS, so it survives a token change14<div className="rounded-[var(--radius-xl)] p-2">15 <div className="rounded-[calc(var(--radius-xl)-8px)]" />16</div>1718// Edge-anchored surfaces round two corners only19<BottomSheet className="rounded-t-[var(--radius-3xl)]" />2021// Clip children that reach the edge22<div className="overflow-hidden rounded-[var(--radius-xl)]">23 <img src={cover} alt="" />24</div>CSS
:root {
--radius-xs: 4px;
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-2xl: 20px;
--radius-3xl: 28px;
}
/* Nesting expressed in CSS so it tracks the token */
.card {
border-radius: var(--radius-xl);
padding: 8px;
}
.card > .card__media {
border-radius: calc(var(--radius-xl) - 8px);
}
/* Clip anything that reaches a rounded edge */
.card--media { overflow: hidden; }
/* Edge-anchored: only the free corners */
.sheet {
border-start-start-radius: var(--radius-3xl);
border-start-end-radius: var(--radius-3xl);
}
/* Logical properties, so RTL mirrors correctly without a second rule */
.notice {
border-start-start-radius: var(--radius-md);
border-end-start-radius: var(--radius-md);
}Professional tips
- When in doubt between two steps, take the smaller one. Under-rounding reads as restrained; over-rounding reads as a toy.
- A border is drawn inside the radius, so a bordered element looks marginally tighter than an unbordered one at the same value. Above 16px, bump the bordered version by 1px if they sit side by side.
- Radius and elevation should move together. A surface that floats higher usually wants a slightly larger radius — compare our 12px popover with our 20px dialog.
- For images inside cards, prefer overflow-hidden on the card to setting a radius on the image. One rule instead of one per child, and it survives content changes.
Performance
- border-radius on a scrolling container forces the browser to clip on every frame. On long lists, round the wrapper rather than the scroll area itself.
- A rounded element with a shadow and a backdrop-filter is three separate compositing steps. That combination is fine on a dialog and expensive on 200 table rows.
- Avoid animating border-radius; it triggers a repaint of the whole element. Cross-fade two elements or animate a transform instead.
Common mistakes
- Forgetting overflow-hidden on a card with a full-bleed image. The image squares off the corners and nobody notices until a screenshot goes into a deck.
- Applying a radius to a full-width mobile banner. On a 360px screen the curve is visible against the edge and looks like a rendering artefact.
- Setting a radius on a <table>. The cells ignore it; you must round the wrapper and clip.
- Using rounded-full on a variable-height element. The moment the content wraps to two lines the radius follows the height and the shape changes.
Real-world recommendations
- Pick the container radius first — it is the most visible surface in the product — then derive the rest of the scale from it. 16px containers imply 8px controls.
- If a designer hands over a Figma file with corner radii of 10, 14 and 18, snap them to 8, 12 and 16 before building. The difference is invisible; the consistency is not.
- On marketing pages you can afford one step larger everywhere. Marketing surfaces are bigger and softer shapes read as friendlier; product surfaces are dense and need tighter geometry.
- Audit for radius drift the same way you audit colour: grep for rounded-[ and border-radius: and check every literal against the scale.