Skip to content
DevDepth

In-depth React internals and CSS layout guides for frontend engineers.

← Back to all articles

In-Depth Article

How `border-radius` Really Works: Percentages, Nested Corners, Overlap, and Transform

Understand how CSS `border-radius` resolves percentages, why nested corners look wrong, how overlap scaling works, and what changes when transforms stretch a rounded box.

Published: Updated: 7 min readlayout-strategy
border-radiuscss-layoutlayout-strategycss-logical-properties

border-radius feels easy right up until it does something you did not expect.

A card looks perfect until you place another rounded card inside it. A pill button keeps its shape with 9999px, but an asymmetric version seems to lose corners. A hover animation that scales a box suddenly makes the rounded edges feel stretched and cheap.

None of that behavior is random. The browser is following a geometry model that most of us only partially remember.

This guide focuses on the questions that matter most in real UI work:

  • what percentage radii are actually relative to
  • why nested rounded corners often look mismatched
  • what happens when radii are too large for the box
  • why transform stretches rounded corners instead of recalculating them
  • how to make rounded corners adapt to different component sizes

If you want the wider layout baseline first, start with Modern CSS Layout: History, Axes, Flow, and the Mental Model You Actually Need.

1. The mental model: each corner can have two radii

The most useful thing to remember about border-radius is that a corner is not always a simple circle quarter. It can be an ellipse quarter.

That means every corner can have:

  • a horizontal radius
  • a vertical radius

When you write:

.box {
  border-radius: 1.5rem;
}

the browser uses the same value for both axes on all four corners.

When you write:

.box {
  border-radius: 4rem 1rem 3rem 2rem / 2rem 3rem 1rem 4rem;
}

the values before the slash are the horizontal radii, and the values after the slash are the vertical radii.

That small detail explains a lot of the strange shapes people see later. border-radius is not just "round the corners." It is "define corner geometry."

2. Percentage radii are resolved separately on each axis

This is the rule that surprises people most often:

The horizontal radius uses the box width. The vertical radius uses the box height.

So when you write:

.blob {
  width: 24rem;
  height: 12rem;
  border-radius: 30% 70% 20% 40%;
}

the 30%, 70%, 20%, and 40% values are not one shared percentage against one shared size.

For the top-left corner, for example:

  • horizontal radius = 24rem * 30%
  • vertical radius = 12rem * 30%

That is why the same percentage value can produce a perfect circle quarter in a square box and an elliptical corner in a rectangular box.

It also explains one of the most common beginner questions:

why does border-radius: 50% produce a circle sometimes and an ellipse other times?

The answer is simple. It only becomes a circle when width and height are equal. Otherwise, the horizontal and vertical radii are resolving against different dimensions.

The slash form follows the same rule:

.blob {
  width: 24rem;
  height: 12rem;
  border-radius: 70% 30% 30% 70% / 60% 40% 60% 40%;
}

Now the browser resolves:

  • the first four percentages against width
  • the second four percentages against height

If a rounded shape feels "wrong," start here. Many border-radius bugs are really dimension bugs.

3. Nested rounded corners require subtraction, not matching numbers

Rounded containers often look slightly off when the inner element uses the same radius as the outer one.

Here is the classic mistake:

<div class="card-shell">
  <div class="card"></div>
</div>
.card-shell {
  border-radius: 32px;
  padding: 16px;
}

.card {
  border-radius: 32px;
}

The numbers match, but the geometry does not.

The inner element starts farther inward because of the shell's padding, so the inner curve needs to be smaller to stay visually tangent to the outer curve.

The practical rule is:

inner radius = outer radius - inset

If the shell also has a border, the rule becomes:

inner radius = outer radius - border width - padding

That gives you a much better nested result:

.card-shell {
  --outer-radius: 32px;
  --inset: 16px;
  --inner-radius: calc(var(--outer-radius) - var(--inset));

  padding: var(--inset);
  border-radius: var(--outer-radius);
}

.card {
  border-radius: max(0px, var(--inner-radius));
}

This same idea also explains what happens inside a single element with border, padding, and border-radius.

If the outer radius is larger than the inward offset, the browser can still form an inner rounded path. Conceptually, you are subtracting the inset from the outer curve. If the result becomes zero or negative, the inner curve stops being visibly rounded.

If your component architecture uses wrapper shells for spacing and decoration, this fits the same ownership model described in When to Use margin, padding, and gap in CSS Layouts: the shell owns the inset, so the inner component must respect that inset when it chooses its own radius.

4. When large radii overlap, the browser scales them down together

This is the rule behind pill buttons, capsules, and "missing corner" bugs.

Suppose you write:

.pill {
  width: 18rem;
  height: 3rem;
  border-radius: 999rem;
}

That works because the browser does not let corner arcs overlap in an impossible way. Instead, it scales the radii until the shape fits the box.

The simplified process is:

  1. Look at each side of the box.
  2. Compare that side length to the sum of the two corner radii touching it.
  3. Find the tightest ratio.
  4. Multiply all corner radii by that ratio if scaling is needed.

That is why a huge radius still becomes a valid pill shape instead of drawing broken geometry.

It also explains this confusing case:

.pill {
  width: 18rem;
  height: 3rem;
  border-radius: 100px 999rem 999rem 100px;
}

At first glance, it can seem like the left corners disappeared. What actually happened is that the right-side radii forced a global scale-down, so every corner radius was reduced together. The small left corners were not ignored. They were shrunk as part of the same correction.

The practical takeaway is:

If one side is too aggressive, every corner may be rescaled.

That is why giant radii are great for pills, but asymmetrical shapes need more deliberate values.

5. transform stretches the finished rounded box instead of recalculating it

Another easy trap is using transform to fake width or height animation:

.panel {
  border-radius: 1rem;
  transition: transform 0.2s ease;
}

.panel:hover {
  transform: scaleX(1.4);
}

The rounded corners do not get recalculated as though the box had a new layout width. The browser computes the rounded shape first, then the transform scales that painted result.

That is why the corner can look visually stretched.

This behavior is not a bug. transform is applied after layout, which is part of why it is such a useful animation tool. But it means the corner geometry follows the transform rather than being recomputed from a fresh box size.

If you need the corner to stay optically correct while the component changes width, prefer changing the actual dimension instead of scaling the whole element:

.panel {
  width: 12rem;
  border-radius: 1rem;
  transition: width 0.2s ease;
}

.panel:hover {
  width: 16rem;
}

That costs more layout work than a pure transform, but it produces a radius that is recalculated against the real box.

For more advanced visual polish, a wrapper-based approach or nine-slice style composition can help, but the core rule is simple: transform scales the result you already had.

6. Direction-safe UI often needs logical corner properties

This part is easy to miss because border-radius shorthand is physical. It assumes top-left, top-right, bottom-right, and bottom-left.

That is fine when all four corners are the same. It becomes fragile when the component needs different corners in both LTR and RTL interfaces.

In those cases, logical corner properties are safer:

.badge {
  border-start-start-radius: 1rem;
  border-start-end-radius: 0.5rem;
  border-end-start-radius: 0.5rem;
  border-end-end-radius: 1rem;
}

That keeps the corner intent tied to logical start and end rather than hard-coded physical left and right.

If direction-safe layout is part of your component system, pair this with LTR, RTL, and CSS Logical Properties: How to Build Direction-Safe Layouts.

7. Responsive radius is easier when you treat it as a constraint

Sometimes the correct corner radius really does change with context.

A large desktop card may look polished with a generous radius, while the same module in a narrow mobile stack wants flatter edges. The cleanest version of that logic is usually a container query:

.card-shell {
  container-type: inline-size;
}

.card {
  border-radius: 0;
}

@container (width > 42rem) {
  .card {
    border-radius: 1rem;
  }
}

That keeps the decision tied to the component's available space instead of the whole viewport.

You can also create fluid radius behavior with min(), max(), or clamp(), but container queries are often easier to understand and maintain because they describe the actual layout condition directly.

If that component-driven mindset is new, Responsive UI Beyond Breakpoints: Component-Driven Design and Container Queries is the best next read.

8. A quick checklist for weird border-radius bugs

When a rounded corner feels wrong, run through these questions in order:

  1. Is a percentage radius resolving against a box with different width and height?
  2. Is a nested child reusing the same radius instead of subtracting the shell's border or padding?
  3. Are oversized radii being scaled down because adjacent corners would overlap?
  4. Is a transform stretching an already-painted corner instead of changing the real box size?
  5. Does the component need logical corner properties because the layout must work in both LTR and RTL?
  6. Should the radius respond to the component's container size rather than to a hardcoded viewport breakpoint?

That checklist solves most "mysterious" rounded-corner bugs much faster than tweaking random values in DevTools.

9. The takeaway

border-radius is easy to type, but it is not a trivial property.

It resolves percentages per axis, subtracts inward space when you build nested corners, rescales itself to avoid overlap, and happily stretches under transforms because the transform happens after the corner geometry is already defined.

If you keep one mental model from this page, make it this one:

border-radius is not just decoration. It is box geometry.

Once you treat it that way, pill buttons, nested cards, responsive components, and transform-driven motion all start making much more sense.

Publisher and editor

DevDepth Publisher

Independent publisher and frontend engineering writer

DevDepth is maintained as an independent frontend engineering publication focused on React internals, CSS layout systems, and production debugging guides.

React internalsCSS layout systemsfrontend debuggingrendering and performance reasoning

Each article is reviewed before publication for technical accuracy, explanation quality, metadata clarity, and internal-link fit within the current archive.

Last editorial review: Mar 20, 2026