Looking for a Front-End Developer and Design Systems Practitioner?

I currently have some availabilty. Let's talk!.

Always Twisted

Context‑Aware Cornering: How inherit() Can Simplify Border‑Radius for Components

On This Page:

Whilst I was working on my last article about the newinherit() CSS function I looked at how we could use this to help simplify the border radius of child elements based on what the parent element had. But, I’d completely forgot about it when I started to write that article so, as a ‘bonus’, I thought I’d quickly write it up here.

The Design Problem

Rounded corners can be a small design detail, but they matter. When you nest elements, like a card with padding containing an inner panel with its own background and then a button inside that, the inner corners can easily look out of whack. The inner corners could appear to too small or too large and fail to look part of a overall consistent design.

Designers want a consistent visual rhythm across any nester layers and site visitors can see this visual consistency as a way to ‘trust’ the site they’re using. Yet, different contexts may change the padding and radius means keeping control of the values to make it visually consistent quite tedious.

The design goal here is pretty simple: nested elements should visually respect the parent border radius (corner) so the UI looks cohesive without the need to add lots of extra design tokens or per-component overrides.

How teams work it out now

Parent publishes derived values

The parent computes and publishes derived variable, for example ⁠--inner-radius and ⁠--btn-radius, and children read those with ⁠var(). This is explicit and compatible, but it pushes the responsibility to the parent and increases the amount of design tokens and/or Sass variables / CSS Custom Properties you’ll need to create.

Code languagecss
.card {
--card-radius: 1.5rem;
--card-padding: 1rem;
--inner-radius: calc(var(--card-radius) - (var(--card-padding) * 0.5));
--btn-radius: calc(var(--inner-radius) - (var(--card-padding) * 0.25));
border-radius: var(--card-radius);
padding: var(--card-padding);
}
.card__inner { border-radius: var(--inner-radius); }
.btn { border-radius: var(--btn-radius); }

Each layer computes locally from published base variables

The parent publishes base variables (⁠—card-radius, ⁠--card-padding). Each child computes its own value using ⁠var() and ⁠calc(). This keeps the parent API smaller but requires each child to know the math.

Code languagecss
.card {
--card-radius: 1.5rem;
--card-padding: 1rem;
}
.card__inner {
--inner-radius: max(0, calc(var(--card-radius) - (var(--card-padding) * 0.5)));
border-radius: var(--inner-radius);
}
.btn {
border-radius: max(0, calc(var(--inner-radius) - (var(--card-padding) * 0.25)));
}

Build-time expansion using Sass

If you want reliable output that works in all browsers and avoids runtime arithmetic, generate the derived radius at build time and emit explicit CSS variables (or classes) for each variant. Sass is a convenient place to do this: generate card variants from a spacing and radius scale, compute inner and button radii using your chosen rules, and output the variables on the card (or as utility classes).

Define your base scales (spacing and radius)

Code languagescss
$spacing: (
100: 0.25rem,
200: 0.5rem,
300: 0.75rem,
400: 1rem,
500: 1.5rem
);
$radius: (
sm: 0.375rem,
md: 0.75rem,
lg: 1.5rem
);

Create a function for the calculations

Code languagescss
@function inner-radius($outer-radius, $pad, $factor: 0.5) {
$result: $outer-radius - ($pad * $factor);
@if $result <= 0 {
@return 0rem;
}
@return $result;
}
@function grandchild-radius($parent-radius, $pad, $factor: 0.25) {
$result: $parent-radius - ($pad * $factor);
@if $result <= 0 {
@return 0rem;
}
@return $result;
}

Create a mixin that can generate the variables

Code languagescss
// Generic mixin: create radii for a parent element.
// - Creates --parent-radius and --parent-pad (base tokens)
// - Creates --inner-radius (derived from parent)
// - Creates --grandchild-radius (derived from inner and pad)
@mixin derived-radii($parent-radius, $parent-pad, $inner-factor: 0.5, $grandchild-factor: 0.25) {
// expose base tokens on the element
--parent-radius: #{$parent-radius};
--parent-pad: #{$parent-pad};
// compute values at build time and create them as CSS variables
$inner: inner-radius($parent-radius, $parent-pad, $inner-factor);
--inner-radius: #{$inner};
$grand: grandchild-radius($inner, $parent-pad, $grandchild-factor);
--grandchild-radius: #{$grand};
border-radius: var(--parent-radius);
padding: var(--parent-pad);
}

Example using a card with a child (card__inner) and grandchild (.btn)

Code languagescss
.card {
--parent-radius: 0.75rem;
--parent-pad: 1rem;
border-radius: var(--parent-radius);
padding: var(--parent-pad);
}
/* apply mixin for one variant */
.card--lg-400 {
@include derived-radii(map-get($radius, lg), map-get($spacing, 400));
}
.card__inner {
border-radius: var(--inner-radius, 0.5rem);
padding: calc(var(--parent-pad, 1rem) * 0.5);
}
.card__inner .btn {
border-radius: var(--grandchild-radius, 0.375rem);
}

Generated CSS

Code languagecss
.card {
--parent-radius: 0.75rem;
--parent-pad: 1rem;
border-radius: var(--parent-radius);
padding: var(--parent-pad);
}
.card--lg-400 {
--parent-radius: 1.5rem;
--parent-pad: 1rem;
--inner-radius: 1rem;
--grandchild-radius: 0.75rem;
border-radius: var(--parent-radius);
padding: var(--parent-pad);
}
.card__inner {
border-radius: var(--inner-radius, 0.5rem);
padding: calc(var(--parent-pad, 1rem) * 0.5);
}
.card__inner .btn {
border-radius: var(--grandchild-radius, 0.375rem);
}

What inherit() is (recap)

As mentioned in the previous article, ⁠inherit() is a working draft CSS Values Level 5 function that will substitute the parent element's computed custom-property

Code languagecss
inherit(--foo, 1rem)

It returns the parent’s computed value for ⁠--foo. If the immediate parent does not set it, it comes from the nearest ancestor. The second argument is an optional fallback used when the inherited value is invalid. Using inherit() lets the component say "trust my parent to decide, I'll work it out from there.”. Using var() says "I want this specific token."

Making Context-Aware Corners with inherit()

With ⁠inherit() each component can perform the math locally, reading exactly what the parent resolved. The child expresses how it wants to be inset relative to its parent. Let’s take a card as an example, which has an inner panel and a button.

Code languagehtml
<article class="card">
<div class="card__inner">
<button class="btn">Action</button>
</div>
</article>
Code languagecss
.card {
--card-radius: 1.5rem;
--card-padding: 1rem;
border-radius: var(--card-radius);
padding: var(--card-padding);
}
/* inner computes its radius directly from the parent's computed values */
.card__inner {
border-radius: max(
0,
calc(
inherit(--card-radius, 1.5rem)
- (inherit(--card-padding, 1rem) * 0.5)
)
);
padding: calc(inherit(--card-padding, 1rem) * 0.5);
}
/* button computes its radius from the inner's computed result and the card pad */
.btn {
border-radius: max(
0,
calc(
inherit(--card-radius, 1.5rem)
- (inherit(--card-padding, 1rem) * (0.5 + 0.25))
)
);
padding: 0.5rem 1rem;
}

inherit(--card-radius) reads the parent element’s computed --card-radius. If the parent computes a local alias (for example —card-pad: calc(var(--gap) * 0.75)), inherit() returns that computed result rather than performing a raw token lookup. All inherit() calls in the examples include fallbacks so styles remain predictable when a token is missing or when the feature is not supported.

Bonus, grandchildren and cleaner code.

For many components it’s cleaner for the inner element to compute and publish one derived value and for grandchildren to consume that value. The inner computes ⁠--inner-radius (using ⁠inherit() and a safe ⁠calc()) and publishes it, Grandchildren then read that single value to compute their own corners. That keeps the math in one place and keeps child rules simple. Creating ⁠--inner-radius should also make intent obvious, wrapping the calculation with ⁠max(, ...) acts as a safety guard. Subtracting padding from a radius can produce a negative value. max(0, ...) forces the result to zero rather than producing an invalid radius or a visual glitch when values change.

Code languagecss
.outer {
--outer-radius: 1.5rem;
--outer-pad: 1rem;
border-radius: var(--outer-radius);
padding: var(--outer-pad);
}
.inner {
--inner-radius: max(0, calc(inherit(--outer-radius, 1.5rem) - (inherit(--outer-pad, 1rem) * 0.5)));
border-radius: var(--inner-radius);
padding: calc(inherit(--outer-pad, 1rem) * 0.5);
}
.btn {
border-radius: max(0, calc(inherit(--inner-radius, 0.75rem) - (inherit(--outer-pad, 1rem) * 0.25)));
}

Why this may be better

Using ⁠inherit() here shifts ownership: parents publish base variables (radius, padding) and components declare how they want to derive values from their parent. That reduces variable surface area and avoids pushing many derived values onto the parent. It also makes components resilient to parent-side transforms, if the parent computes an alias or applies clamp/scale, children using ⁠inherit() follow the computed result.

Components also become resilient to parent-side transforms. If a parent computes an alias or applies clamp or scaling, children using ⁠inherit() automatically follow the parent’s computed value. The result is cleaner, more self-documenting component CSS: the relationship (calculate from parent) is explicit in the component, and derived definitions are not scattered across the codebase.

Are you struggling to define what your Design System should include to serve your team effectively?

I can audit your needs and create a tailored Design System roadmap.

get in touch!