Pure TailwindMIT licensed

Tailwind Ripple Effect Button with No JavaScript

A Material-style ripple that fires on press with no JavaScript at all. The port's interesting line is active:after:animate-tab03-ripple — two variants stacked, reading "while pressed, run the ripple on my own after-layer", which is the entire behaviour in one class.

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="tab-03 w-full min-h-svh grid place-items-center bg-tab03-bg font-tab03-sans px-6 py-10">
  <div class="flex flex-col items-center gap-8">
    <p class="text-[0.78rem] tracking-[0.14em] uppercase text-tab03-violet/70 m-0">Click or tap to trigger</p>
    <div class="flex flex-wrap justify-center gap-4">
      <button type="button" class="relative overflow-hidden min-h-11 px-8 text-[0.95rem] font-bold tracking-[0.03em] text-white bg-tab03-violet rounded-[10px] cursor-pointer transition-[filter] duration-200 hover:brightness-110 active:brightness-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab03-violet after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:size-0 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-full after:bg-white/40 after:opacity-0 after:pointer-events-none active:after:animate-tab03-ripple motion-reduce:active:after:animate-none">Get Started</button>
      <button type="button" class="relative overflow-hidden min-h-11 px-8 text-[0.95rem] font-bold tracking-[0.03em] text-white bg-tab03-teal rounded-[10px] cursor-pointer transition-[filter] duration-200 hover:brightness-110 active:brightness-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab03-teal after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:size-0 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-full after:bg-white/40 after:opacity-0 after:pointer-events-none active:after:animate-tab03-ripple motion-reduce:active:after:animate-none">Learn More</button>
      <button type="button" class="relative overflow-hidden min-h-11 px-8 text-[0.95rem] font-bold tracking-[0.03em] text-white bg-tab03-rose rounded-[10px] cursor-pointer transition-[filter] duration-200 hover:brightness-110 active:brightness-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab03-rose after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:size-0 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-full after:bg-white/40 after:opacity-0 after:pointer-events-none active:after:animate-tab03-ripple motion-reduce:active:after:animate-none">Subscribe</button>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tab03-bg: #0f0c1a;
  --color-tab03-bg: oklch(0.14 0.03 295);
  --color-tab03-violet: #7c3aed;
  --color-tab03-violet: oklch(0.53 0.25 295);
  --color-tab03-teal: #0d9488;
  --color-tab03-teal: oklch(0.56 0.11 184);
  --color-tab03-rose: #e11d48;
  --color-tab03-rose: oklch(0.57 0.22 13);
  --font-tab03-sans: 'Space Grotesk', system-ui, sans-serif;
  --animate-tab03-ripple: tab03-ripple 0.6s linear forwards;
}

@keyframes tab03-ripple {
  0% {
    width: 0;
    height: 0;
    opacity: 0.4;
  }

  80% {
    width: 400%;
    height: 400%;
    opacity: 0.1;
  }

  100% {
    width: 500%;
    height: 500%;
    opacity: 0;
  }
}

.tab-03 {
  width: 100%;
  min-height: 100svh;
}

.tab-03 ::selection {
  background: var(--color-tab03-violet);
  color: #fff;
}

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-tab03-bg: #0f0c1a;
  --color-tab03-bg: oklch(0.14 0.03 295);
  --color-tab03-violet: #7c3aed;
  --color-tab03-violet: oklch(0.53 0.25 295);
  --color-tab03-teal: #0d9488;
  --color-tab03-teal: oklch(0.56 0.11 184);
  --color-tab03-rose: #e11d48;
  --color-tab03-rose: oklch(0.57 0.22 13);
  --font-tab03-sans: 'Space Grotesk', system-ui, sans-serif;
  --animate-tab03-ripple: tab03-ripple 0.6s linear forwards;
}

@keyframes tab03-ripple {
  0% {
    width: 0;
    height: 0;
    opacity: 0.4;
  }

  80% {
    width: 400%;
    height: 400%;
    opacity: 0.1;
  }

  100% {
    width: 500%;
    height: 500%;
    opacity: 0;
  }
}

.tab-03 {
  width: 100%;
  min-height: 100svh;
}

.tab-03 ::selection {
  background: var(--color-tab03-violet);
  color: #fff;
}
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=Space+Grotesk:wght@400;500;600;700&display=swap">

What changed in the port

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

  • active:after: is the port's whole story: a stacked variant expresses :active::after exactly, with no loss.
  • The keyframe animates width/height rather than scale, matching the original. Scaling a fixed-size disk would be cheaper, but it changes the easing feel — a faithful port keeps the original's motion.
  • UI change from the CSS original: per-variant focus-visible rings, so the button is not keyboard-invisible while being press-reactive.
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 Ripple Effect Button with No JavaScript
Source: https://codefronts.com/motion/tailwind-css-animated-buttons/tailwind-ripple-effect-button-no-javascript/

A Material-style ripple that fires on press with no JavaScript at all. The port's interesting line is active:after:animate-tab03-ripple &mdash; two variants stacked, reading "while pressed, run the ripple on my own after-layer", which is the entire behaviour in one class.
## HTML
```html
<section class="tab-03 w-full min-h-svh grid place-items-center bg-tab03-bg font-tab03-sans px-6 py-10">
  <div class="flex flex-col items-center gap-8">
    <p class="text-[0.78rem] tracking-[0.14em] uppercase text-tab03-violet/70 m-0">Click or tap to trigger</p>
    <div class="flex flex-wrap justify-center gap-4">
      <button type="button" class="relative overflow-hidden min-h-11 px-8 text-[0.95rem] font-bold tracking-[0.03em] text-white bg-tab03-violet rounded-[10px] cursor-pointer transition-[filter] duration-200 hover:brightness-110 active:brightness-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab03-violet after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:size-0 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-full after:bg-white/40 after:opacity-0 after:pointer-events-none active:after:animate-tab03-ripple motion-reduce:active:after:animate-none">Get Started</button>
      <button type="button" class="relative overflow-hidden min-h-11 px-8 text-[0.95rem] font-bold tracking-[0.03em] text-white bg-tab03-teal rounded-[10px] cursor-pointer transition-[filter] duration-200 hover:brightness-110 active:brightness-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab03-teal after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:size-0 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-full after:bg-white/40 after:opacity-0 after:pointer-events-none active:after:animate-tab03-ripple motion-reduce:active:after:animate-none">Learn More</button>
      <button type="button" class="relative overflow-hidden min-h-11 px-8 text-[0.95rem] font-bold tracking-[0.03em] text-white bg-tab03-rose rounded-[10px] cursor-pointer transition-[filter] duration-200 hover:brightness-110 active:brightness-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab03-rose after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:size-0 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-full after:bg-white/40 after:opacity-0 after:pointer-events-none active:after:animate-tab03-ripple motion-reduce:active:after:animate-none">Subscribe</button>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tab03-bg: #0f0c1a;
  --color-tab03-bg: oklch(0.14 0.03 295);
  --color-tab03-violet: #7c3aed;
  --color-tab03-violet: oklch(0.53 0.25 295);
  --color-tab03-teal: #0d9488;
  --color-tab03-teal: oklch(0.56 0.11 184);
  --color-tab03-rose: #e11d48;
  --color-tab03-rose: oklch(0.57 0.22 13);
  --font-tab03-sans: 'Space Grotesk', system-ui, sans-serif;
  --animate-tab03-ripple: tab03-ripple 0.6s linear forwards;
}

@keyframes tab03-ripple {
  0% {
    width: 0;
    height: 0;
    opacity: 0.4;
  }

  80% {
    width: 400%;
    height: 400%;
    opacity: 0.1;
  }

  100% {
    width: 500%;
    height: 500%;
    opacity: 0;
  }
}

.tab-03 {
  width: 100%;
  min-height: 100svh;
}

.tab-03 ::selection {
  background: var(--color-tab03-violet);
  color: #fff;
}
```

How this works

Two variants, one behaviour. active: and after: compose left-to-right, so active:after:animate-tab03-ripple compiles to .btn:active::after{animation:…}. The keyframe grows the disk to 500% while fading opacity, and because it is bound to :active it resets on release with no cleanup code.

The seed disk is four utilities. after:size-0 after:rounded-full plus the two centring translates put the origin at the button centre at any size. The ripple is white at 40% alpha, which reads on all three fills.

It still cannot follow the cursor. That is a property of the technique, not the port: a pure-CSS ripple has no access to pointer coordinates. Demo 20 is the cursor-aware version, and it needs JS for exactly that reason.

Make it yours

  • Origin: swap the centring translates for after:top-[80%] after:left-[10%] to burst from a corner.
  • Speed lives in the token — --animate-tab03-ripple: tab03-ripple 0.6s linear forwards.
  • On light fills, change the disk to after:bg-black/10 so the ripple stays visible.

Gotchas — read before shipping

  • iOS Safari only fires :active on elements that look interactive — keep cursor-pointer on the button or the ripple never plays on iPhone.
  • overflow-hidden is what makes the pill shape clip the disk. Remove it and a 500% circle escapes the button on all four sides.
  • Do not animate size on the button itself — only the pseudo-element. The keyframe touches layout, and confining it to the after-layer is what keeps that cheap.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

:active and keyframe animations are universally supported; the floor is Tailwind v4's generated sheet.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Search CodeFronts

Loading…