Pure TailwindMIT licensed

Tailwind Ink Splash Radio Animation

A Material-style ink ripple reimagined for radio rows: checking an option splashes ink from the indicator until it floods the row, while the dot stamps in like a wet press. The splash is the row's before: circle, scaled 0 → 34 by has-checked: with an asymmetric transition, so ink soaks in slowly and dries fast.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="trb-10 w-full min-h-svh grid place-items-center bg-trb10-bg px-6 py-14 font-trb10-sans text-trb10-body" aria-label="Ink splash radio buttons demo">
  <fieldset class="w-full max-w-[26rem]">
    <legend class="font-trb10-serif text-[30px] leading-tight font-medium tracking-[-0.01em]">Paper stock</legend>
    <p class="mt-1 text-sm text-trb10-mut">Printed on FSC-certified stock.</p>
    <div class="mt-5 grid gap-2">
      <label class="group relative isolate flex min-h-16 cursor-pointer items-center gap-3.5 overflow-hidden rounded-2xl border-[1.5px] border-trb10-line bg-trb10-card px-[18px] transition-colors duration-300 before:pointer-events-none before:absolute before:top-1/2 before:left-7 before:-z-10 before:-m-[15px] before:size-[30px] before:scale-0 before:rounded-full before:bg-[radial-gradient(circle,oklch(0.45_0.16_265/0.12),oklch(0.45_0.16_265/0.09)_55%,oklch(0.45_0.16_265/0.07)_80%,transparent_97%)] before:opacity-0 before:[transition:scale_.2s,opacity_.3s] before:content-[''] hover:border-trb10-ink/35 has-checked:border-trb10-ink/55 has-checked:before:scale-[34] has-checked:before:opacity-100 has-checked:before:[transition:scale_.8s_cubic-bezier(.22,1,.36,1),opacity_.2s] has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-trb10-ink motion-reduce:has-checked:before:[transition:opacity_.2s]">
        <input type="radio" name="trb-10" value="matte" class="sr-only" checked>
        <span aria-hidden="true" class="grid size-[22px] flex-none place-items-center rounded-full border-2 border-trb10-ring transition-colors duration-300 after:size-[11px] after:scale-0 after:rounded-full after:bg-trb10-ink after:transition-transform after:duration-350 after:ease-trb10-stamp after:content-[''] group-has-checked:border-trb10-ink group-has-checked:after:scale-100 motion-reduce:after:transition-none"></span>
        <span class="grid flex-1"><span class="font-trb10-serif text-lg font-semibold">Matte</span><span class="text-[13px] text-trb10-mut">Soft, no glare</span></span>
        <span class="text-xs font-semibold tracking-[0.06em] text-trb10-mut uppercase">120 gsm</span>
      </label>
      <label class="group relative isolate flex min-h-16 cursor-pointer items-center gap-3.5 overflow-hidden rounded-2xl border-[1.5px] border-trb10-line bg-trb10-card px-[18px] transition-colors duration-300 before:pointer-events-none before:absolute before:top-1/2 before:left-7 before:-z-10 before:-m-[15px] before:size-[30px] before:scale-0 before:rounded-full before:bg-[radial-gradient(circle,oklch(0.45_0.16_265/0.12),oklch(0.45_0.16_265/0.09)_55%,oklch(0.45_0.16_265/0.07)_80%,transparent_97%)] before:opacity-0 before:[transition:scale_.2s,opacity_.3s] before:content-[''] hover:border-trb10-ink/35 has-checked:border-trb10-ink/55 has-checked:before:scale-[34] has-checked:before:opacity-100 has-checked:before:[transition:scale_.8s_cubic-bezier(.22,1,.36,1),opacity_.2s] has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-trb10-ink motion-reduce:has-checked:before:[transition:opacity_.2s]">
        <input type="radio" name="trb-10" value="satin" class="sr-only">
        <span aria-hidden="true" class="grid size-[22px] flex-none place-items-center rounded-full border-2 border-trb10-ring transition-colors duration-300 after:size-[11px] after:scale-0 after:rounded-full after:bg-trb10-ink after:transition-transform after:duration-350 after:ease-trb10-stamp after:content-[''] group-has-checked:border-trb10-ink group-has-checked:after:scale-100 motion-reduce:after:transition-none"></span>
        <span class="grid flex-1"><span class="font-trb10-serif text-lg font-semibold">Satin</span><span class="text-[13px] text-trb10-mut">Gentle sheen</span></span>
        <span class="text-xs font-semibold tracking-[0.06em] text-trb10-mut uppercase">150 gsm</span>
      </label>
      <label class="group relative isolate flex min-h-16 cursor-pointer items-center gap-3.5 overflow-hidden rounded-2xl border-[1.5px] border-trb10-line bg-trb10-card px-[18px] transition-colors duration-300 before:pointer-events-none before:absolute before:top-1/2 before:left-7 before:-z-10 before:-m-[15px] before:size-[30px] before:scale-0 before:rounded-full before:bg-[radial-gradient(circle,oklch(0.45_0.16_265/0.12),oklch(0.45_0.16_265/0.09)_55%,oklch(0.45_0.16_265/0.07)_80%,transparent_97%)] before:opacity-0 before:[transition:scale_.2s,opacity_.3s] before:content-[''] hover:border-trb10-ink/35 has-checked:border-trb10-ink/55 has-checked:before:scale-[34] has-checked:before:opacity-100 has-checked:before:[transition:scale_.8s_cubic-bezier(.22,1,.36,1),opacity_.2s] has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-trb10-ink motion-reduce:has-checked:before:[transition:opacity_.2s]">
        <input type="radio" name="trb-10" value="gloss" class="sr-only">
        <span aria-hidden="true" class="grid size-[22px] flex-none place-items-center rounded-full border-2 border-trb10-ring transition-colors duration-300 after:size-[11px] after:scale-0 after:rounded-full after:bg-trb10-ink after:transition-transform after:duration-350 after:ease-trb10-stamp after:content-[''] group-has-checked:border-trb10-ink group-has-checked:after:scale-100 motion-reduce:after:transition-none"></span>
        <span class="grid flex-1"><span class="font-trb10-serif text-lg font-semibold">Gloss</span><span class="text-[13px] text-trb10-mut">Vivid, reflective</span></span>
        <span class="text-xs font-semibold tracking-[0.06em] text-trb10-mut uppercase">170 gsm</span>
      </label>
    </div>
  </fieldset>
</section>
@import "tailwindcss";

@theme {
  --color-trb10-bg: #f6f1e7;
  --color-trb10-bg: oklch(0.958 0.016 85);
  --color-trb10-card: #fffdf8;
  --color-trb10-card: oklch(0.995 0.006 85);
  --color-trb10-body: #2a2620;
  --color-trb10-body: oklch(0.27 0.012 75);
  --color-trb10-mut: #6a6150;
  --color-trb10-mut: oklch(0.5 0.025 80);
  --color-trb10-line: #e7dfd0;
  --color-trb10-line: oklch(0.91 0.02 85);
  --color-trb10-ring: #a99d84;
  --color-trb10-ring: oklch(0.7 0.03 85);
  --color-trb10-ink: #3a47a8;
  --color-trb10-ink: oklch(0.45 0.16 265);
  --ease-trb10-stamp: cubic-bezier(0.34, 1.7, 0.5, 1);
  --font-trb10-serif: 'Newsreader', Georgia, serif;
  --font-trb10-sans: 'Geist', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.trb-10 {
  width: 100%;
  min-height: 100svh;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-trb10-bg: #f6f1e7;
  --color-trb10-bg: oklch(0.958 0.016 85);
  --color-trb10-card: #fffdf8;
  --color-trb10-card: oklch(0.995 0.006 85);
  --color-trb10-body: #2a2620;
  --color-trb10-body: oklch(0.27 0.012 75);
  --color-trb10-mut: #6a6150;
  --color-trb10-mut: oklch(0.5 0.025 80);
  --color-trb10-line: #e7dfd0;
  --color-trb10-line: oklch(0.91 0.02 85);
  --color-trb10-ring: #a99d84;
  --color-trb10-ring: oklch(0.7 0.03 85);
  --color-trb10-ink: #3a47a8;
  --color-trb10-ink: oklch(0.45 0.16 265);
  --ease-trb10-stamp: cubic-bezier(0.34, 1.7, 0.5, 1);
  --font-trb10-serif: 'Newsreader', Georgia, serif;
  --font-trb10-sans: 'Geist', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.trb-10 {
  width: 100%;
  min-height: 100svh;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400..700;1,6..72,400..700&family=Geist:wght@400..700&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • The splash moved from a .row::before rule to before: utilities on the row; the two-speed transition is two arbitrary [transition:…] values, one per state.
  • isolate + before:-z-10 replaced the original's per-child z-index:1 rules — the ink sits under all content in one stroke.
  • UI upgrade from the CSS original: a trailing paper weight (120 / 150 / 170 gsm) gives the choice a measurable difference, and titles moved to Newsreader for a print-shop voice.
  • UI upgrade: sublines rose from #8d8471 (3.5:1) to #6a6150 (5.8:1 on the ink wash), and the ring from 2.2:1 to 3.1:1.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Ink Splash Radio Animation
Source: https://codefronts.com/components/tailwind-css-radio-buttons/tailwind-ink-splash-radio-animation/

A Material-style ink ripple reimagined for radio rows: checking an option splashes ink from the indicator until it floods the row, while the dot stamps in like a wet press. The splash is the row's before: circle, scaled 0 → 34 by has-checked: with an asymmetric transition, so ink soaks in slowly and dries fast.
## HTML
```html
<section class="trb-10 w-full min-h-svh grid place-items-center bg-trb10-bg px-6 py-14 font-trb10-sans text-trb10-body" aria-label="Ink splash radio buttons demo">
  <fieldset class="w-full max-w-[26rem]">
    <legend class="font-trb10-serif text-[30px] leading-tight font-medium tracking-[-0.01em]">Paper stock</legend>
    <p class="mt-1 text-sm text-trb10-mut">Printed on FSC-certified stock.</p>
    <div class="mt-5 grid gap-2">
      <label class="group relative isolate flex min-h-16 cursor-pointer items-center gap-3.5 overflow-hidden rounded-2xl border-[1.5px] border-trb10-line bg-trb10-card px-[18px] transition-colors duration-300 before:pointer-events-none before:absolute before:top-1/2 before:left-7 before:-z-10 before:-m-[15px] before:size-[30px] before:scale-0 before:rounded-full before:bg-[radial-gradient(circle,oklch(0.45_0.16_265/0.12),oklch(0.45_0.16_265/0.09)_55%,oklch(0.45_0.16_265/0.07)_80%,transparent_97%)] before:opacity-0 before:[transition:scale_.2s,opacity_.3s] before:content-[''] hover:border-trb10-ink/35 has-checked:border-trb10-ink/55 has-checked:before:scale-[34] has-checked:before:opacity-100 has-checked:before:[transition:scale_.8s_cubic-bezier(.22,1,.36,1),opacity_.2s] has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-trb10-ink motion-reduce:has-checked:before:[transition:opacity_.2s]">
        <input type="radio" name="trb-10" value="matte" class="sr-only" checked>
        <span aria-hidden="true" class="grid size-[22px] flex-none place-items-center rounded-full border-2 border-trb10-ring transition-colors duration-300 after:size-[11px] after:scale-0 after:rounded-full after:bg-trb10-ink after:transition-transform after:duration-350 after:ease-trb10-stamp after:content-[''] group-has-checked:border-trb10-ink group-has-checked:after:scale-100 motion-reduce:after:transition-none"></span>
        <span class="grid flex-1"><span class="font-trb10-serif text-lg font-semibold">Matte</span><span class="text-[13px] text-trb10-mut">Soft, no glare</span></span>
        <span class="text-xs font-semibold tracking-[0.06em] text-trb10-mut uppercase">120 gsm</span>
      </label>
      <label class="group relative isolate flex min-h-16 cursor-pointer items-center gap-3.5 overflow-hidden rounded-2xl border-[1.5px] border-trb10-line bg-trb10-card px-[18px] transition-colors duration-300 before:pointer-events-none before:absolute before:top-1/2 before:left-7 before:-z-10 before:-m-[15px] before:size-[30px] before:scale-0 before:rounded-full before:bg-[radial-gradient(circle,oklch(0.45_0.16_265/0.12),oklch(0.45_0.16_265/0.09)_55%,oklch(0.45_0.16_265/0.07)_80%,transparent_97%)] before:opacity-0 before:[transition:scale_.2s,opacity_.3s] before:content-[''] hover:border-trb10-ink/35 has-checked:border-trb10-ink/55 has-checked:before:scale-[34] has-checked:before:opacity-100 has-checked:before:[transition:scale_.8s_cubic-bezier(.22,1,.36,1),opacity_.2s] has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-trb10-ink motion-reduce:has-checked:before:[transition:opacity_.2s]">
        <input type="radio" name="trb-10" value="satin" class="sr-only">
        <span aria-hidden="true" class="grid size-[22px] flex-none place-items-center rounded-full border-2 border-trb10-ring transition-colors duration-300 after:size-[11px] after:scale-0 after:rounded-full after:bg-trb10-ink after:transition-transform after:duration-350 after:ease-trb10-stamp after:content-[''] group-has-checked:border-trb10-ink group-has-checked:after:scale-100 motion-reduce:after:transition-none"></span>
        <span class="grid flex-1"><span class="font-trb10-serif text-lg font-semibold">Satin</span><span class="text-[13px] text-trb10-mut">Gentle sheen</span></span>
        <span class="text-xs font-semibold tracking-[0.06em] text-trb10-mut uppercase">150 gsm</span>
      </label>
      <label class="group relative isolate flex min-h-16 cursor-pointer items-center gap-3.5 overflow-hidden rounded-2xl border-[1.5px] border-trb10-line bg-trb10-card px-[18px] transition-colors duration-300 before:pointer-events-none before:absolute before:top-1/2 before:left-7 before:-z-10 before:-m-[15px] before:size-[30px] before:scale-0 before:rounded-full before:bg-[radial-gradient(circle,oklch(0.45_0.16_265/0.12),oklch(0.45_0.16_265/0.09)_55%,oklch(0.45_0.16_265/0.07)_80%,transparent_97%)] before:opacity-0 before:[transition:scale_.2s,opacity_.3s] before:content-[''] hover:border-trb10-ink/35 has-checked:border-trb10-ink/55 has-checked:before:scale-[34] has-checked:before:opacity-100 has-checked:before:[transition:scale_.8s_cubic-bezier(.22,1,.36,1),opacity_.2s] has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-trb10-ink motion-reduce:has-checked:before:[transition:opacity_.2s]">
        <input type="radio" name="trb-10" value="gloss" class="sr-only">
        <span aria-hidden="true" class="grid size-[22px] flex-none place-items-center rounded-full border-2 border-trb10-ring transition-colors duration-300 after:size-[11px] after:scale-0 after:rounded-full after:bg-trb10-ink after:transition-transform after:duration-350 after:ease-trb10-stamp after:content-[''] group-has-checked:border-trb10-ink group-has-checked:after:scale-100 motion-reduce:after:transition-none"></span>
        <span class="grid flex-1"><span class="font-trb10-serif text-lg font-semibold">Gloss</span><span class="text-[13px] text-trb10-mut">Vivid, reflective</span></span>
        <span class="text-xs font-semibold tracking-[0.06em] text-trb10-mut uppercase">170 gsm</span>
      </label>
    </div>
  </fieldset>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-trb10-bg: #f6f1e7;
  --color-trb10-bg: oklch(0.958 0.016 85);
  --color-trb10-card: #fffdf8;
  --color-trb10-card: oklch(0.995 0.006 85);
  --color-trb10-body: #2a2620;
  --color-trb10-body: oklch(0.27 0.012 75);
  --color-trb10-mut: #6a6150;
  --color-trb10-mut: oklch(0.5 0.025 80);
  --color-trb10-line: #e7dfd0;
  --color-trb10-line: oklch(0.91 0.02 85);
  --color-trb10-ring: #a99d84;
  --color-trb10-ring: oklch(0.7 0.03 85);
  --color-trb10-ink: #3a47a8;
  --color-trb10-ink: oklch(0.45 0.16 265);
  --ease-trb10-stamp: cubic-bezier(0.34, 1.7, 0.5, 1);
  --font-trb10-serif: 'Newsreader', Georgia, serif;
  --font-trb10-sans: 'Geist', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.trb-10 {
  width: 100%;
  min-height: 100svh;
}
```

How this works

Asymmetric timing is the effect. Tailwind's duration-* sets one duration for every property. The soak needs a slow scale and a fast opacity, so both states carry an arbitrary [transition:…] — the checked state's value applies while entering, the base value while leaving.

Sized to clear the far corner. A 30px circle at scale-[34] is 1020px across — enough to reach the far edge of a 26rem row from the indicator. Undershoot and the wash dies mid-row as a visible band.

Make it yours

  • Ink colour: --color-trb10-ink; the wash derives from it at 7–12% alpha.
  • Faster soak: shorten .8s in the checked transition.
  • Per-row inks: override --color-trb10-ink on a label with an arbitrary property.

Gotchas — read before shipping

  • The row needs overflow-hidden or the ink shows square outside the rounded corners.
  • v4's scale-* writes the scale property — transition scale, not transform.
  • Keep the wash under ~12% so text contrast survives on top of it.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Without :has() the rows select instantly, with no splash.

Techniques used in this demo

Search CodeFronts

Loading…