12 / 30
All 30 in 30 Tailwind CSS Hover Effects
- 01Tailwind Underline Draw Hover Effect
- 02Tailwind Text Glitch Hover Effect
- 03Tailwind Letter Spacing Expand Hover Effect
- 04Tailwind Gradient Text Reveal Hover Effect
- 05Tailwind Split Text Hover Effect
- 06Tailwind Neon Glow Text Hover Effect
- 07Tailwind Magnetic Liquid Button Hover Effect
- 08Tailwind Border Draw Hover Effect
- 09Tailwind Shimmer Shine Button Hover Effect
- 10Tailwind Fill Wipe Button Hover Effect
- 11Tailwind 3D Press Button Hover Effect
- 12Tailwind Ripple Button Hover Effect
- 13Tailwind 3D Tilt Card Hover Effect
- 14Tailwind Card Flip Hover Effect
- 15Tailwind Glassmorphism Card Hover Effect
- 16Tailwind Spotlight Card Hover Effect
- 17Tailwind Slide Reveal Card Hover Effect
- 18Tailwind Stack Lift Card Hover Effect
- 19Tailwind Zoom Pan Image Hover Effect
- 20Tailwind Color Channel Split Hover Effect
- 21Tailwind Duotone Image Hover Effect
- 22Tailwind Curtain Reveal Image Hover Effect
- 23Tailwind Ken Burns Image Hover Effect
- 24Tailwind Image Tilt Depth Hover Effect
- 25Tailwind Underline Slide Nav Hover Effect
- 26Tailwind Highlight Fill Nav Hover Effect
- 27Tailwind Strikethrough Hover Link Effect
- 28Tailwind Inline Word Swap Hover Effect
- 29Tailwind Dot Trail Cursor Hover Effect
- 30Tailwind Magnetic Cursor Pull Hover Effect
Tailwind Ripple Button Hover Effect
Five impact effects — centre ripple, staggered double ring, contained wave, outline-offset ping and a box-shadow glow ring. Ripples are one-shot forwards animations minted as --animate-* tokens; the ping and glow need no animation at all.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="thv-12 w-full min-h-svh grid place-items-center bg-thv12-bg font-thv12-sans px-6 py-14">
<div class="w-[min(680px,100%)] flex flex-col gap-4">
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="relative shrink-0 min-h-11 px-9 py-3 rounded-lg border-0 bg-thv12-pink text-[0.95rem] font-semibold tracking-[0.05em] text-white cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-pink after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:w-full after:h-full after:rounded-full after:bg-white/40 after:-translate-x-1/2 after:-translate-y-1/2 after:scale-0 after:pointer-events-none hover:after:animate-thv12-ripple focus-visible:after:animate-thv12-ripple motion-reduce:hover:after:animate-none">Center Ripple</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">scale + opacity keyframe</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="relative shrink-0 min-h-11 px-9 py-3 rounded-lg border-0 bg-thv12-rose text-[0.95rem] font-semibold tracking-[0.05em] text-white cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-rose before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:w-full before:h-full before:rounded-full before:bg-white/30 before:-translate-x-1/2 before:-translate-y-1/2 before:scale-0 before:pointer-events-none after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:w-full after:h-full after:rounded-full after:bg-white/20 after:-translate-x-1/2 after:-translate-y-1/2 after:scale-0 after:pointer-events-none hover:before:animate-thv12-ripple hover:after:animate-thv12-ripple-late focus-visible:before:animate-thv12-ripple focus-visible:after:animate-thv12-ripple-late motion-reduce:hover:before:animate-none motion-reduce:hover:after:animate-none">Double Ring</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">staggered dual ripple</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="relative overflow-hidden shrink-0 min-h-11 px-9 py-3 rounded-lg border-2 border-thv12-magenta bg-transparent text-[0.95rem] font-semibold tracking-[0.05em] text-thv12-magenta cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-magenta before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:w-0 before:h-0 before:rounded-full before:bg-thv12-magenta/25 before:-translate-x-1/2 before:-translate-y-1/2 before:pointer-events-none hover:before:animate-thv12-wave focus-visible:before:animate-thv12-wave motion-reduce:hover:before:animate-none">Contained Wave</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">overflow-clipped wave</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="shrink-0 min-h-11 px-9 py-3 rounded-lg border-0 bg-thv12-purple text-[0.95rem] font-semibold tracking-[0.05em] text-white cursor-pointer outline-2 outline-thv12-purple outline-offset-0 transition-[outline-offset,outline-color] duration-400 hover:outline-offset-[10px] hover:outline-transparent focus-visible:outline-offset-[10px] motion-reduce:transition-none">Border Pulse</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">outline-offset ping</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="shrink-0 min-h-11 px-9 py-3 rounded-lg border-2 border-thv12-pink bg-transparent text-[0.95rem] font-semibold tracking-[0.05em] text-thv12-pink cursor-pointer shadow-[0_0_0_0_rgba(236,72,153,.4)] transition-[box-shadow,color,background-color] duration-400 ease-[cubic-bezier(.4,0,.2,1)] hover:text-white hover:bg-thv12-pink hover:shadow-[0_0_0_12px_rgba(236,72,153,0)] focus-visible:text-white focus-visible:bg-thv12-pink focus-visible:shadow-[0_0_0_12px_rgba(236,72,153,0)] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-pink motion-reduce:transition-none">Glow Ring</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">box-shadow expand</span>
</div>
</div>
</section><section class="thv-12 w-full min-h-svh grid place-items-center bg-thv12-bg font-thv12-sans px-6 py-14">
<div class="w-[min(680px,100%)] flex flex-col gap-4">
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="relative shrink-0 min-h-11 px-9 py-3 rounded-lg border-0 bg-thv12-pink text-[0.95rem] font-semibold tracking-[0.05em] text-white cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-pink after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:w-full after:h-full after:rounded-full after:bg-white/40 after:-translate-x-1/2 after:-translate-y-1/2 after:scale-0 after:pointer-events-none hover:after:animate-thv12-ripple focus-visible:after:animate-thv12-ripple motion-reduce:hover:after:animate-none">Center Ripple</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">scale + opacity keyframe</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="relative shrink-0 min-h-11 px-9 py-3 rounded-lg border-0 bg-thv12-rose text-[0.95rem] font-semibold tracking-[0.05em] text-white cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-rose before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:w-full before:h-full before:rounded-full before:bg-white/30 before:-translate-x-1/2 before:-translate-y-1/2 before:scale-0 before:pointer-events-none after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:w-full after:h-full after:rounded-full after:bg-white/20 after:-translate-x-1/2 after:-translate-y-1/2 after:scale-0 after:pointer-events-none hover:before:animate-thv12-ripple hover:after:animate-thv12-ripple-late focus-visible:before:animate-thv12-ripple focus-visible:after:animate-thv12-ripple-late motion-reduce:hover:before:animate-none motion-reduce:hover:after:animate-none">Double Ring</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">staggered dual ripple</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="relative overflow-hidden shrink-0 min-h-11 px-9 py-3 rounded-lg border-2 border-thv12-magenta bg-transparent text-[0.95rem] font-semibold tracking-[0.05em] text-thv12-magenta cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-magenta before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:w-0 before:h-0 before:rounded-full before:bg-thv12-magenta/25 before:-translate-x-1/2 before:-translate-y-1/2 before:pointer-events-none hover:before:animate-thv12-wave focus-visible:before:animate-thv12-wave motion-reduce:hover:before:animate-none">Contained Wave</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">overflow-clipped wave</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="shrink-0 min-h-11 px-9 py-3 rounded-lg border-0 bg-thv12-purple text-[0.95rem] font-semibold tracking-[0.05em] text-white cursor-pointer outline-2 outline-thv12-purple outline-offset-0 transition-[outline-offset,outline-color] duration-400 hover:outline-offset-[10px] hover:outline-transparent focus-visible:outline-offset-[10px] motion-reduce:transition-none">Border Pulse</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">outline-offset ping</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="shrink-0 min-h-11 px-9 py-3 rounded-lg border-2 border-thv12-pink bg-transparent text-[0.95rem] font-semibold tracking-[0.05em] text-thv12-pink cursor-pointer shadow-[0_0_0_0_rgba(236,72,153,.4)] transition-[box-shadow,color,background-color] duration-400 ease-[cubic-bezier(.4,0,.2,1)] hover:text-white hover:bg-thv12-pink hover:shadow-[0_0_0_12px_rgba(236,72,153,0)] focus-visible:text-white focus-visible:bg-thv12-pink focus-visible:shadow-[0_0_0_12px_rgba(236,72,153,0)] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-pink motion-reduce:transition-none">Glow Ring</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">box-shadow expand</span>
</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-thv12-bg: #0f0008;
--color-thv12-bg: oklch(0.12 0.04 350);
--color-thv12-dim: #6b7280;
--color-thv12-dim: oklch(0.55 0.02 265);
--color-thv12-pink: #ec4899;
--color-thv12-pink: oklch(0.65 0.23 355);
--color-thv12-rose: #f43f5e;
--color-thv12-rose: oklch(0.65 0.23 16);
--color-thv12-magenta: #db2777;
--color-thv12-magenta: oklch(0.58 0.23 355);
--color-thv12-purple: #a855f7;
--color-thv12-purple: oklch(0.65 0.25 303);
--font-thv12-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv12-ripple: thv12-ripple .6s ease-out forwards;
--animate-thv12-ripple-late: thv12-ripple .7s ease-out .25s forwards;
--animate-thv12-wave: thv12-wave .6s ease-out forwards;
}
.thv-12 {
width: 100%;
min-height: 100svh;
}
.thv-12 ::selection {
background: var(--color-thv12-magenta);
color: #fff;
}
@keyframes thv12-ripple {
0% {
scale: 0;
opacity: .7;
}
100% {
scale: 3.5;
opacity: 0;
}
}
@keyframes thv12-wave {
0% {
width: 0;
height: 0;
opacity: .8;
}
100% {
width: 300px;
height: 300px;
opacity: 0;
}
}@import "tailwindcss";
@theme {
--color-thv12-bg: #0f0008;
--color-thv12-bg: oklch(0.12 0.04 350);
--color-thv12-dim: #6b7280;
--color-thv12-dim: oklch(0.55 0.02 265);
--color-thv12-pink: #ec4899;
--color-thv12-pink: oklch(0.65 0.23 355);
--color-thv12-rose: #f43f5e;
--color-thv12-rose: oklch(0.65 0.23 16);
--color-thv12-magenta: #db2777;
--color-thv12-magenta: oklch(0.58 0.23 355);
--color-thv12-purple: #a855f7;
--color-thv12-purple: oklch(0.65 0.25 303);
--font-thv12-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv12-ripple: thv12-ripple .6s ease-out forwards;
--animate-thv12-ripple-late: thv12-ripple .7s ease-out .25s forwards;
--animate-thv12-wave: thv12-wave .6s ease-out forwards;
}
.thv-12 {
width: 100%;
min-height: 100svh;
}
.thv-12 ::selection {
background: var(--color-thv12-magenta);
color: #fff;
}
@keyframes thv12-ripple {
0% {
scale: 0;
opacity: .7;
}
100% {
scale: 3.5;
opacity: 0;
}
}
@keyframes thv12-wave {
0% {
width: 0;
height: 0;
opacity: .8;
}
100% {
width: 300px;
height: 300px;
opacity: 0;
}
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-thv12-bg: #0f0008;
--color-thv12-bg: oklch(0.12 0.04 350);
--color-thv12-dim: #6b7280;
--color-thv12-dim: oklch(0.55 0.02 265);
--color-thv12-pink: #ec4899;
--color-thv12-pink: oklch(0.65 0.23 355);
--color-thv12-rose: #f43f5e;
--color-thv12-rose: oklch(0.65 0.23 16);
--color-thv12-magenta: #db2777;
--color-thv12-magenta: oklch(0.58 0.23 355);
--color-thv12-purple: #a855f7;
--color-thv12-purple: oklch(0.65 0.25 303);
--font-thv12-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv12-ripple: thv12-ripple .6s ease-out forwards;
--animate-thv12-ripple-late: thv12-ripple .7s ease-out .25s forwards;
--animate-thv12-wave: thv12-wave .6s ease-out forwards;
}
.thv-12 {
width: 100%;
min-height: 100svh;
}
.thv-12 ::selection {
background: var(--color-thv12-magenta);
color: #fff;
}
@keyframes thv12-ripple {
0% {
scale: 0;
opacity: .7;
}
100% {
scale: 3.5;
opacity: 0;
}
}
@keyframes thv12-wave {
0% {
width: 0;
height: 0;
opacity: .8;
}
100% {
width: 300px;
height: 300px;
opacity: 0;
}
}@import "tailwindcss";
@theme {
--color-thv12-bg: #0f0008;
--color-thv12-bg: oklch(0.12 0.04 350);
--color-thv12-dim: #6b7280;
--color-thv12-dim: oklch(0.55 0.02 265);
--color-thv12-pink: #ec4899;
--color-thv12-pink: oklch(0.65 0.23 355);
--color-thv12-rose: #f43f5e;
--color-thv12-rose: oklch(0.65 0.23 16);
--color-thv12-magenta: #db2777;
--color-thv12-magenta: oklch(0.58 0.23 355);
--color-thv12-purple: #a855f7;
--color-thv12-purple: oklch(0.65 0.25 303);
--font-thv12-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv12-ripple: thv12-ripple .6s ease-out forwards;
--animate-thv12-ripple-late: thv12-ripple .7s ease-out .25s forwards;
--animate-thv12-wave: thv12-wave .6s ease-out forwards;
}
.thv-12 {
width: 100%;
min-height: 100svh;
}
.thv-12 ::selection {
background: var(--color-thv12-magenta);
color: #fff;
}
@keyframes thv12-ripple {
0% {
scale: 0;
opacity: .7;
}
100% {
scale: 3.5;
opacity: 0;
}
}
@keyframes thv12-wave {
0% {
width: 0;
height: 0;
opacity: .8;
}
100% {
width: 300px;
height: 300px;
opacity: 0;
}
}<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&family=JetBrains+Mono:wght@400;500;600;800&display=swap">
<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&family=JetBrains+Mono:wght@400;500;600;800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Registering one keyframe twice with different delays is the neatest trick in this entry — the CSS version needed a second selector to express the same stagger.
- UI change from the CSS original: all five ripple on
focus-visible:, so keyboard activation gets the same impact feedback a mouse does. motion-reduce:silences the ripples but leaves the glow-ring and colour changes intact, so no button loses its hover state entirely.
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 Button Hover Effect
Source: https://codefronts.com/motion/tailwind-css-hover-effects/tailwind-ripple-button-hover-effect/
Five impact effects — centre ripple, staggered double ring, contained wave, outline-offset ping and a box-shadow glow ring. Ripples are one-shot forwards animations minted as --animate-* tokens; the ping and glow need no animation at all.
## HTML
```html
<section class="thv-12 w-full min-h-svh grid place-items-center bg-thv12-bg font-thv12-sans px-6 py-14">
<div class="w-[min(680px,100%)] flex flex-col gap-4">
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="relative shrink-0 min-h-11 px-9 py-3 rounded-lg border-0 bg-thv12-pink text-[0.95rem] font-semibold tracking-[0.05em] text-white cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-pink after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:w-full after:h-full after:rounded-full after:bg-white/40 after:-translate-x-1/2 after:-translate-y-1/2 after:scale-0 after:pointer-events-none hover:after:animate-thv12-ripple focus-visible:after:animate-thv12-ripple motion-reduce:hover:after:animate-none">Center Ripple</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">scale + opacity keyframe</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="relative shrink-0 min-h-11 px-9 py-3 rounded-lg border-0 bg-thv12-rose text-[0.95rem] font-semibold tracking-[0.05em] text-white cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-rose before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:w-full before:h-full before:rounded-full before:bg-white/30 before:-translate-x-1/2 before:-translate-y-1/2 before:scale-0 before:pointer-events-none after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:w-full after:h-full after:rounded-full after:bg-white/20 after:-translate-x-1/2 after:-translate-y-1/2 after:scale-0 after:pointer-events-none hover:before:animate-thv12-ripple hover:after:animate-thv12-ripple-late focus-visible:before:animate-thv12-ripple focus-visible:after:animate-thv12-ripple-late motion-reduce:hover:before:animate-none motion-reduce:hover:after:animate-none">Double Ring</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">staggered dual ripple</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="relative overflow-hidden shrink-0 min-h-11 px-9 py-3 rounded-lg border-2 border-thv12-magenta bg-transparent text-[0.95rem] font-semibold tracking-[0.05em] text-thv12-magenta cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-magenta before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:w-0 before:h-0 before:rounded-full before:bg-thv12-magenta/25 before:-translate-x-1/2 before:-translate-y-1/2 before:pointer-events-none hover:before:animate-thv12-wave focus-visible:before:animate-thv12-wave motion-reduce:hover:before:animate-none">Contained Wave</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">overflow-clipped wave</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="shrink-0 min-h-11 px-9 py-3 rounded-lg border-0 bg-thv12-purple text-[0.95rem] font-semibold tracking-[0.05em] text-white cursor-pointer outline-2 outline-thv12-purple outline-offset-0 transition-[outline-offset,outline-color] duration-400 hover:outline-offset-[10px] hover:outline-transparent focus-visible:outline-offset-[10px] motion-reduce:transition-none">Border Pulse</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">outline-offset ping</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="shrink-0 min-h-11 px-9 py-3 rounded-lg border-2 border-thv12-pink bg-transparent text-[0.95rem] font-semibold tracking-[0.05em] text-thv12-pink cursor-pointer shadow-[0_0_0_0_rgba(236,72,153,.4)] transition-[box-shadow,color,background-color] duration-400 ease-[cubic-bezier(.4,0,.2,1)] hover:text-white hover:bg-thv12-pink hover:shadow-[0_0_0_12px_rgba(236,72,153,0)] focus-visible:text-white focus-visible:bg-thv12-pink focus-visible:shadow-[0_0_0_12px_rgba(236,72,153,0)] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-pink motion-reduce:transition-none">Glow Ring</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">box-shadow expand</span>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-thv12-bg: #0f0008;
--color-thv12-bg: oklch(0.12 0.04 350);
--color-thv12-dim: #6b7280;
--color-thv12-dim: oklch(0.55 0.02 265);
--color-thv12-pink: #ec4899;
--color-thv12-pink: oklch(0.65 0.23 355);
--color-thv12-rose: #f43f5e;
--color-thv12-rose: oklch(0.65 0.23 16);
--color-thv12-magenta: #db2777;
--color-thv12-magenta: oklch(0.58 0.23 355);
--color-thv12-purple: #a855f7;
--color-thv12-purple: oklch(0.65 0.25 303);
--font-thv12-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv12-ripple: thv12-ripple .6s ease-out forwards;
--animate-thv12-ripple-late: thv12-ripple .7s ease-out .25s forwards;
--animate-thv12-wave: thv12-wave .6s ease-out forwards;
}
.thv-12 {
width: 100%;
min-height: 100svh;
}
.thv-12 ::selection {
background: var(--color-thv12-magenta);
color: #fff;
}
@keyframes thv12-ripple {
0% {
scale: 0;
opacity: .7;
}
100% {
scale: 3.5;
opacity: 0;
}
}
@keyframes thv12-wave {
0% {
width: 0;
height: 0;
opacity: .8;
}
100% {
width: 300px;
height: 300px;
opacity: 0;
}
}
```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 Button Hover Effect
Source: https://codefronts.com/motion/tailwind-css-hover-effects/tailwind-ripple-button-hover-effect/
Five impact effects — centre ripple, staggered double ring, contained wave, outline-offset ping and a box-shadow glow ring. Ripples are one-shot forwards animations minted as --animate-* tokens; the ping and glow need no animation at all.
## HTML
```html
<section class="thv-12 w-full min-h-svh grid place-items-center bg-thv12-bg font-thv12-sans px-6 py-14">
<div class="w-[min(680px,100%)] flex flex-col gap-4">
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="relative shrink-0 min-h-11 px-9 py-3 rounded-lg border-0 bg-thv12-pink text-[0.95rem] font-semibold tracking-[0.05em] text-white cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-pink after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:w-full after:h-full after:rounded-full after:bg-white/40 after:-translate-x-1/2 after:-translate-y-1/2 after:scale-0 after:pointer-events-none hover:after:animate-thv12-ripple focus-visible:after:animate-thv12-ripple motion-reduce:hover:after:animate-none">Center Ripple</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">scale + opacity keyframe</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="relative shrink-0 min-h-11 px-9 py-3 rounded-lg border-0 bg-thv12-rose text-[0.95rem] font-semibold tracking-[0.05em] text-white cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-rose before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:w-full before:h-full before:rounded-full before:bg-white/30 before:-translate-x-1/2 before:-translate-y-1/2 before:scale-0 before:pointer-events-none after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:w-full after:h-full after:rounded-full after:bg-white/20 after:-translate-x-1/2 after:-translate-y-1/2 after:scale-0 after:pointer-events-none hover:before:animate-thv12-ripple hover:after:animate-thv12-ripple-late focus-visible:before:animate-thv12-ripple focus-visible:after:animate-thv12-ripple-late motion-reduce:hover:before:animate-none motion-reduce:hover:after:animate-none">Double Ring</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">staggered dual ripple</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="relative overflow-hidden shrink-0 min-h-11 px-9 py-3 rounded-lg border-2 border-thv12-magenta bg-transparent text-[0.95rem] font-semibold tracking-[0.05em] text-thv12-magenta cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-magenta before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:w-0 before:h-0 before:rounded-full before:bg-thv12-magenta/25 before:-translate-x-1/2 before:-translate-y-1/2 before:pointer-events-none hover:before:animate-thv12-wave focus-visible:before:animate-thv12-wave motion-reduce:hover:before:animate-none">Contained Wave</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">overflow-clipped wave</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="shrink-0 min-h-11 px-9 py-3 rounded-lg border-0 bg-thv12-purple text-[0.95rem] font-semibold tracking-[0.05em] text-white cursor-pointer outline-2 outline-thv12-purple outline-offset-0 transition-[outline-offset,outline-color] duration-400 hover:outline-offset-[10px] hover:outline-transparent focus-visible:outline-offset-[10px] motion-reduce:transition-none">Border Pulse</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">outline-offset ping</span>
</div>
<div class="flex flex-wrap items-center justify-between gap-6 px-9 py-6 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<button type="button" class="shrink-0 min-h-11 px-9 py-3 rounded-lg border-2 border-thv12-pink bg-transparent text-[0.95rem] font-semibold tracking-[0.05em] text-thv12-pink cursor-pointer shadow-[0_0_0_0_rgba(236,72,153,.4)] transition-[box-shadow,color,background-color] duration-400 ease-[cubic-bezier(.4,0,.2,1)] hover:text-white hover:bg-thv12-pink hover:shadow-[0_0_0_12px_rgba(236,72,153,0)] focus-visible:text-white focus-visible:bg-thv12-pink focus-visible:shadow-[0_0_0_12px_rgba(236,72,153,0)] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-thv12-pink motion-reduce:transition-none">Glow Ring</button>
<span class="shrink-0 text-[11px] tracking-[0.12em] uppercase text-thv12-dim">box-shadow expand</span>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-thv12-bg: #0f0008;
--color-thv12-bg: oklch(0.12 0.04 350);
--color-thv12-dim: #6b7280;
--color-thv12-dim: oklch(0.55 0.02 265);
--color-thv12-pink: #ec4899;
--color-thv12-pink: oklch(0.65 0.23 355);
--color-thv12-rose: #f43f5e;
--color-thv12-rose: oklch(0.65 0.23 16);
--color-thv12-magenta: #db2777;
--color-thv12-magenta: oklch(0.58 0.23 355);
--color-thv12-purple: #a855f7;
--color-thv12-purple: oklch(0.65 0.25 303);
--font-thv12-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv12-ripple: thv12-ripple .6s ease-out forwards;
--animate-thv12-ripple-late: thv12-ripple .7s ease-out .25s forwards;
--animate-thv12-wave: thv12-wave .6s ease-out forwards;
}
.thv-12 {
width: 100%;
min-height: 100svh;
}
.thv-12 ::selection {
background: var(--color-thv12-magenta);
color: #fff;
}
@keyframes thv12-ripple {
0% {
scale: 0;
opacity: .7;
}
100% {
scale: 3.5;
opacity: 0;
}
}
@keyframes thv12-wave {
0% {
width: 0;
height: 0;
opacity: .8;
}
100% {
width: 300px;
height: 300px;
opacity: 0;
}
}
```How this works
One keyframe, two tokens. The double ring uses the same thv12-ripple keyframe registered twice with different delays, so the CSS stays one block and the difference is a theme entry.
forwards matters. A ripple that reverses on mouse-out looks like a mistake; forwards lets the ring finish its expansion and disappear at full radius.
Two of the five are pure state. The ping animates outline-offset and the glow animates a shadow spread — both transitions, both compositor-friendly, neither needing a pseudo-element.
Make it yours
- Recolour the ring by editing the pseudo-element's
after:bg-white/40. - Outline-only ripple: swap the fill for
after:border-2 after:border-white/60 after:bg-transparent. - Speed: change the
@themeduration —.3sreads reactive,.8smeditative.
Gotchas — read before shipping
- The ripple element must be a circle with equal width and height plus
rounded-full; percentage sizes inherit the button's aspect ratio and produce an ellipse. - Firefox's
outline-offsettransition can step rather than glide; the glow-ring variant is the consistent fallback. - Ripples are decorative — they carry
pointer-events-noneso they never eat the click.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Scale and opacity animations are compositor-only; outline-offset transitions have minor Firefox quirks.