A border-radius is just a number. A number can't describe how a corner should feel.
The same 16px looks almost fully rounded on a button, but very sharp on a large card. Give a nested image the same radius as its parent, and the corners still won't align visually.
That's because roundness isn't absolute. It shifts with the height of the element, the padding inside it, the border around it. The number stays the same. The relationship changes.
Most design systems define radius tokens. Far fewer define the relationships between them.
Nested corners
The common failure is one element rounded inside another: a card at 12px, an image inside it, 8px of padding.
Passes review, looks wrong on screen. Nested corners need a shared center. The inner corner is not another 12px corner placed inside the card. It is the same corner moved inward by the gap.1
That is why the inner radius is the outer radius minus the gap. The gap is everything between the two curves: padding, border, and any intentional spacing.
If the gap is bigger than the radius, the inner corner is square. Once you see it, you start noticing it everywhere.
Borders count as gap
A border is not decoration outside the math. It consumes space between the outer and inner curve. Stack a bordered child inside a padded card and the border joins the padding as part of the gap.
Everything between the two curves is the gap.
Make the relationship explicit
Tokens are useful for choosing the outer radius. They are not enough for nested UI. Once an element sits inside another rounded element, the inner radius should be derived, not picked.
This is the practical rule. Anything nested takes its parent’s radius minus the full gap.
.card { --radius: 12px; --border: 1px; --pad: 8px; border: var(--border) solid; padding: var(--pad); border-radius: var(--radius);}.card__inner { border-radius: calc(var(--radius) - var(--border) - var(--pad));}That buys more coherence than another token name ever will. Hold onto the formula, but not too tightly.
The rule depends on shape
The subtraction works because the default corner is a quarter circle. Offset a circle inward and you get a smaller circle. That is a fact about circles, not corners.
CSS made the circle feel invisible because round was the default. Chromium’s corner-shape support makes the assumption visible.
.card { border-radius: 12px;}@supports (corner-shape: squircle) { .card { corner-shape: squircle; }}The border-radius property still sets how big the corner is. corner-shape sets the curve that fills it.2 round is the arc every example above assumes. squircle is a superellipse-like curve, which changes the geometry.
A superellipse does not offset to a smaller superellipse the way a circle does. Subtract the gap and the inner corner no longer traces the outer one.
The CSS compiles. The numbers look right. The corners drift. There is an open CSSWG issue about a superellipse value with inner and outer symmetry because the plain one does not nest cleanly.3
SwiftUI already has ConcentricRectangle for this. CSS does not. For now the squircle part is a forecast, not a recipe.
Afterthoughts
A corner radius is relative. On its own it is just a number, and the number that looks right depends on the size of the thing it wraps and the gap around what it holds. Nested corners only sit clean when they share a center, which makes the inner radius the outer radius minus the gap, borders counted. All of that assumes the corner is a circle, and the moment you switch to a squircle the subtraction stops holding, with no native fix on the web yet.
The checklist for clean corners:
- Size the radius to the element. A button and a panel should not share one token.
- Nest concentric. Inner radius is the outer radius minus the gap.
- Count the border in the gap. Padding is not the whole distance.
- Re-tune by eye when you squircle. The subtraction does not survive corner-shape.
- Gate
corner-shapebehind@supports. Fall back to round everywhere else. - Animate transform and opacity, never the corner shape. Redrawing the curve every frame is expensive.