Skip to content

Concentric corners for Tailwind CSS

Nested rounded corners only look right when they share a center. Give a card a 24px radius and 12px of padding, and the image inside needs a 12px radius, not 24px. tailwindcss-concentric does that math for you.

Add rounded-concentric to the child. It reads the radius, padding and border from the classes you already wrote on the parent, subtracts in CSS, and keeps working through every breakpoint, state and nesting level.

rounded-[36px] p-3 border
rounded-[36px]
L1 36pxL2 36px
rounded-concentric
L1 36pxL2 23px
Same markup on both sides. The left copies the parent's radius onto every child.

How it works

Two rounded shapes look concentric when their corners share a center point. That happens when the inner radius equals the outer radius minus the space between the two edges.

inner radius=outer radius−gap

The gap is the parent's padding and border width, plus a padded wrapper in between and the child's own margin. For an absolutely positioned child it is the inset. rounded-concentric reads all of it from the classes you already wrote, does the subtraction in CSS, and publishes its own radius so the next level down can do the same.

When the gap is bigger than the radius, the corner goes square instead of negative. A rounded-full parent stays a pill all the way down.

Install

The quickest way is to let your coding agent do it. Add the agent skill at the root of a project that uses Tailwind CSS v4.

Agent skill
npx skills add diip3sh/tailwindcss-concentric

Then ask in plain words. The skill tells the agent what the plugin does, so it can install the package, import it, set up tailwind-merge, pick the right classes and run the audit.

Prompts
Set up tailwindcss-concentric in this project.
Audit nested border radii in src/ and fix them.
Build a pricing card with an inset image and concentric corners.

To set it up by hand, add the package.

Terminal
npm i tailwindcss-concentric

Import it after Tailwind. There is no config file and no JavaScript.

app.css
@import "tailwindcss";
@import "tailwindcss-concentric";

Then add one class to any child of a rounded element.

card.html
<div class="rounded-3xl border p-3">
  <img class="rounded-concentric" />  <!-- 24 − (12 + 1) = 11px -->
</div>

To find the places in an existing codebase that need it, run the audit. It prints each one with a suggested class.

Audit
npx tailwindcss-concentric audit src

Playground

Every radius below comes from the plugin. The root sets rounded-(--lab-r) and its padding from CSS variables. Each inner level only adds rounded-concentric.

The table reads the result back with getComputedStyle, so it shows what the browser rendered, next to the formula it should match.

L4
Outer radius
Padding x
Padding y
Border width
Nested levels
Change the values to see how every level adapts
Radius per level, computed by the browser and by the formula
LevelComputedFormula
L1…set on the root
L2…64 − 20 = 44differs
L3…44 − 20 = 24differs
L4…24 − 20 = 4differs

The same settings as Tailwind classes:

<div class="rounded-[64px] p-5">
  <div class="rounded-concentric p-5">
    <div class="rounded-concentric p-5">
      <div class="rounded-concentric p-5">…</div>
    </div>
  </div>
</div>

In real components

Each example keeps the classes you would already write on the parent. Only the child changes, from a copied radius to rounded-concentric.

Field notes · 6 min read

A weekend above the tree line

Notes, routes and a packing list from three days on the ridge.

rounded-t-concentric: 28px minus 6px padding and a 1px border is 21px

The parent keeps its rounded-*, p-* and border-* classes. Change any of them, at any breakpoint, and the child follows.

API

Every utility comes in the same shapes as Tailwind's own radius utilities.

rounded-concentric
All four corners follow the nearest rounded ancestor: its radius minus the gap between the two edges.
rounded-{t,r,b,l}-concentric
One side only. The other corners keep whatever radius they already have.
rounded-{tl,tr,br,bl}-concentric
One corner only.
rounded-{s,e,ss,se,es,ee}-concentric
Logical sides and corners. They flip with the nearest dir attribute.
rounded-concentric-3rounded-concentric-[6px]rounded-concentric-(--gap)
An explicit gap that replaces the measured one, for spacing the plugin can't read, like custom CSS.
concentric-elliptical
Keeps one radius per axis when the horizontal and vertical gaps differ.
concentric-squircle
Sets corner-shape: squircle and scales the gap to match, where the browser supports it. Elsewhere the corner stays round.
rounded-*p-*border-*m-*inset-*
Nothing new on the parent or the child. Theme, arbitrary and variable values all work, including variants like md: and hover:.

The math is r = max(floor, R − min(gap x, gap y)). The gap is the parent's padding and border width, plus the padding and border of a wrapper in between and the child's own margin. For absolute and fixed children it is the inset plus the parent's border width.

Set a floor

Set --concentric-floor to stop small radii from collapsing to a sharp corner. It inherits like any custom property, so you can set it once or per element.

app.css
:root {
  --concentric-floor: 2px;
}
tight.html
<div class="[--concentric-floor:4px] rounded-lg p-4">
  <div class="rounded-concentric">…</div>  <!-- 4px instead of 0 -->
</div>

tailwind-merge

Register the concentric classes so they conflict with rounded-* the way any other radius would. This is the only JavaScript in the package, and it is optional.

cn.ts
import { extendTailwindMerge } from "tailwind-merge";
import { withConcentric } from "tailwindcss-concentric/merge";

export const twMerge = extendTailwindMerge(withConcentric);

twMerge("rounded-xl rounded-concentric"); // "rounded-concentric"

Agent skill

The repository ships an agent skill that teaches coding agents the plugin's rules. Ask it to set up the plugin and it installs the package and adds the import. Ask for an audit and it walks your components, reports every nested radius and rewrites the ones it can prove.

npx skills add diip3sh/tailwindcss-concentric

Run the command at the root of your project. The skill works with Claude Code, Codex, Cursor, Gemini CLI and other agents that read skills.

Then ask for an audit:

agent session
❯ Audit nested border radii in src/ and app/ and fix them.Scanned 214 files · 6 nested radii · 5 fixed · 1 to reviewsrc/components/card.tsx:14    rounded-t-2xl   → rounded-t-concentricsrc/components/dialog.tsx:42  rounded-xl      → rounded-concentricsrc/components/search.tsx:27  rounded-lg      → rounded-concentricsrc/ui/segmented.tsx:19       rounded-md      → rounded-concentricapp/pricing/page.tsx:88       rounded-[28px]  → rounded-concentricsrc/components/toast.tsx:31   rounded-lg      → reviewReview toast.tsx: the padding comes from custom CSS.

It only rewrites a radius when the classes prove the result. Anything else, like the custom CSS padding in toast.tsx, is left for you to review.

Limitations

  • Only the nearest wrapper's padding and border count on each side, so a second padded wrapper is skipped, and so are margins on wrappers. Give the outer wrapper rounded-concentric, as shown below.
  • Only Tailwind classes are read. Padding from inline styles or custom CSS needs rounded-concentric-{n}.
  • Percentage radii are not supported. Fraction insets like inset-x-1/4 fall back to the parent's padding, and auto margins count as 0.
  • Absolute and fixed elements are measured from the nearest rounded ancestor, so that ancestor should be their containing block.
  • concentric-squircle covers corner-shape: squircle. Other corner shapes have no gap scale yet.
  • Direction comes from dir attributes, including dir="auto" at any depth in browsers with @scope. Older browsers read up to three nested levels. CSS-only direction is not detected.
  • Nine stacked levels are exact. Every element whose class contains rounded counts; deeper ones reuse the last level.

The first two have a one-class fix. A wrapper in the chain passes its padding on, however many wrappers sit inside it. Without a background or border, its own radius is invisible.

rounded-3xl border › p-2 › p-1 › rounded-concentric
p-2
19px
19px
rounded-concentric p-2
11px
11px
Only the inner wrapper's 4px reaches the items. With the outer wrapper in the chain they get 24 − 1 − 8 − 4 = 11px.

Padding the plugin can't read needs an explicit gap. rounded-concentric-3 subtracts 12px wherever the padding comes from.

rounded-3xl style="padding: 12px" › panel
rounded-concentric
24px
rounded-concentric-3
12px
The padding is an inline style, so on the left the panel sees no gap and keeps the card's 24px.

FAQ

Why not just pick a smaller radius?

A hardcoded smaller radius is only right for one combination of values. It breaks as soon as the padding, the border or the outer radius changes, and it doesn't compose across nesting levels or breakpoints. rounded-concentric recalculates for every one of them.

Does it ship any JavaScript?

No. It is a CSS-first Tailwind v4 plugin made of registered custom properties and utilities. The tailwind-merge helper is the only JavaScript, and it is optional.

Which browsers are supported?

The same as Tailwind v4: Chrome 111, Safari 16.4 and Firefox 128 or newer. The plugin relies on @property, which all of them support. Browsers with @scope (Chrome 118, Safari 17.4, Firefox 146) also follow dir="auto" and direction changes nested at any depth.

What happens when the gap is bigger than the radius?

The child gets a square corner, or --concentric-floor if you set one. A rounded-full parent stays a pill all the way down.

How much CSS does it add?

About 3.2 KB gzipped in a minified build: the custom properties and the nesting rules, plus the declarations of the utilities you use. Each nesting level costs about 20 bytes, so there is no smaller build to choose.

How do I find radii that are already wrong?

Run npx tailwindcss-concentric audit src. It reads the class attributes in HTML, JSX, Vue, Svelte and Astro files and lists nested radii that are off, hard-coded calc() radii and wrappers the plugin can't see. It exits with 1 when it finds something, so it can run in CI.