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.
.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.
.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)
$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
@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
// 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
)
.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
.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
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.
<article class="card"><div class="card__inner"><button class="btn">Action</button></div></article>
.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.
.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.