29 / 33
All 33 in 33 Tailwind CSS Card Hover Effects
- 01Tailwind 3D Tilt & Parallax Card with Depth Layers
- 02Tailwind Glowing Gradient & Glassmorphic Border Card
- 03Tailwind Image Zoom Card with Slide-Up Content Reveal
- 04Tailwind 3D Flip Card (Front to Back) with backface-visibility
- 05Tailwind Sibling De-emphasis Pricing Grid with has-hover
- 06Tailwind Minimalist Elevation Card with Layered Dynamic Shadows
- 07Tailwind Elastic Lift Card with Spring Cubic-Bezier
- 08Tailwind Animated Conic Gradient Border Card with @property
- 09Tailwind Stacked Layer Reveal Card (Fanned Risograph Deck)
- 10Tailwind Holographic Foil Shimmer Card with mix-blend-mode
- 11Tailwind Curtain Reveal Card (Parting Velvet Halves)
- 12Tailwind Corner Peel Card with clip-path Triangles
- 13Tailwind Neon Sign Card with Layered Text-Shadow Glow
- 14Tailwind CRT Scanline Terminal Card with Sweeping Beam
- 15Tailwind Depth Parallax Landscape Card with Layer Multipliers
- 16Tailwind Cursor Spotlight Card with Masked Border Highlight
- 17Tailwind 3D Tilt Product Card with Specular Glare
- 18Tailwind Slide-Up Panel Card with Peek Header Swap
- 19Tailwind Credit Card Flip with Magnetic Strip & CVV
- 20Tailwind RGB Channel-Split Glitch Card
- 21Tailwind Rotating Conic Aura Card with Blurred Gradient Bloom
- 22Tailwind Blob-to-Rectangle Border Morph Card
- 23Tailwind Magnetic Float Orb Card with Pointer Attraction
- 24Tailwind Typewriter Reveal Card with Steps Animation & Caret
- 25Tailwind Magazine Overlay Slide Card (Full-Face Panel Takeover)
- 26Tailwind Breathing Film Grain Card with SVG Turbulence Noise
- 27Tailwind Frosted Glass Intensify Card with Deepening Backdrop Blur
- 28Tailwind Blueprint Reveal Card with Circular clip-path Wipe
- 29Tailwind Cursor-Anchored Shockwave Ripple Card
- 30Tailwind Color-Burn Ignition Card with Grayscale-to-Fire Hover
- 31Tailwind Prismatic Light Sweep Card with Screen Blend Bar
- 32Tailwind Book Cover Open Card with Hinged 3D Rotation
- 33Tailwind Drifting Aurora Ribbons Card with Blurred Gradient Bands
Tailwind Cursor-Anchored Shockwave Ripple Card
Two concentric rings fire outward from the exact point the cursor enters the card, the second trailing the first by 120ms. The anchoring is the whole trick: a pointerenter handler writes the entry point into two custom properties, and the rings' origin reads them.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tch-29 w-full min-h-svh grid place-items-center bg-tch29-bg font-tch29-sans p-8">
<article data-tch29="root" class="group relative w-[340px] h-[440px] rounded-[18px] overflow-hidden cursor-pointer bg-[radial-gradient(circle_at_50%_40%,#0e2236,#060d16)] border border-tch29-ring-a/20
before:content-[''] before:absolute before:z-1 before:size-2.5 before:rounded-full before:pointer-events-none before:top-[var(--tch29-y,50%)] before:left-[var(--tch29-x,50%)] before:-translate-x-1/2 before:-translate-y-1/2 before:scale-0 before:border-2 before:border-tch29-ring-a/70 before:shadow-[0_0_30px_var(--color-tch29-ring-a)] group-hover:before:animate-tch29-ring-a motion-reduce:group-hover:before:animate-none
after:content-[''] after:absolute after:z-1 after:size-2.5 after:rounded-full after:pointer-events-none after:top-[var(--tch29-y,50%)] after:left-[var(--tch29-x,50%)] after:-translate-x-1/2 after:-translate-y-1/2 after:scale-0 after:border after:border-tch29-ring-b/60 group-hover:after:animate-tch29-ring-b motion-reduce:group-hover:after:animate-none">
<div class="relative z-2 h-full p-[2.4rem] flex flex-col justify-between text-tch29-ink">
<span class="font-tch29-display text-[0.72rem] tracking-[0.24em] uppercase text-tch29-ring-a">// Energy Core</span>
<h2 class="font-tch29-display font-bold text-[2.6rem] leading-[0.95]">Pulse<br>Reactor</h2>
<p class="text-[0.86rem] leading-[1.6] text-tch29-body">Every interaction sends a wave outward. Hover anywhere — the ripple starts where you enter.</p>
<a href="#" class="self-start inline-flex items-center min-h-11 px-6 rounded-lg font-tch29-display text-[0.85rem] tracking-[0.06em] no-underline bg-tch29-ring-a/12 border border-tch29-ring-a/40 text-tch29-ring-a transition-colors duration-200 hover:bg-tch29-ring-a/22 focus-visible:outline-2 focus-visible:outline-tch29-ring-a focus-visible:outline-offset-2">Discharge ⚡</a>
</div>
</article>
</section><section class="tch-29 w-full min-h-svh grid place-items-center bg-tch29-bg font-tch29-sans p-8">
<article data-tch29="root" class="group relative w-[340px] h-[440px] rounded-[18px] overflow-hidden cursor-pointer bg-[radial-gradient(circle_at_50%_40%,#0e2236,#060d16)] border border-tch29-ring-a/20
before:content-[''] before:absolute before:z-1 before:size-2.5 before:rounded-full before:pointer-events-none before:top-[var(--tch29-y,50%)] before:left-[var(--tch29-x,50%)] before:-translate-x-1/2 before:-translate-y-1/2 before:scale-0 before:border-2 before:border-tch29-ring-a/70 before:shadow-[0_0_30px_var(--color-tch29-ring-a)] group-hover:before:animate-tch29-ring-a motion-reduce:group-hover:before:animate-none
after:content-[''] after:absolute after:z-1 after:size-2.5 after:rounded-full after:pointer-events-none after:top-[var(--tch29-y,50%)] after:left-[var(--tch29-x,50%)] after:-translate-x-1/2 after:-translate-y-1/2 after:scale-0 after:border after:border-tch29-ring-b/60 group-hover:after:animate-tch29-ring-b motion-reduce:group-hover:after:animate-none">
<div class="relative z-2 h-full p-[2.4rem] flex flex-col justify-between text-tch29-ink">
<span class="font-tch29-display text-[0.72rem] tracking-[0.24em] uppercase text-tch29-ring-a">// Energy Core</span>
<h2 class="font-tch29-display font-bold text-[2.6rem] leading-[0.95]">Pulse<br>Reactor</h2>
<p class="text-[0.86rem] leading-[1.6] text-tch29-body">Every interaction sends a wave outward. Hover anywhere — the ripple starts where you enter.</p>
<a href="#" class="self-start inline-flex items-center min-h-11 px-6 rounded-lg font-tch29-display text-[0.85rem] tracking-[0.06em] no-underline bg-tch29-ring-a/12 border border-tch29-ring-a/40 text-tch29-ring-a transition-colors duration-200 hover:bg-tch29-ring-a/22 focus-visible:outline-2 focus-visible:outline-tch29-ring-a focus-visible:outline-offset-2">Discharge ⚡</a>
</div>
</article>
</section>@import "tailwindcss";
@keyframes tch29-ring {
0% {
transform: translate(-50%,-50%) scale(0);
opacity: 0.9;
}
100% {
transform: translate(-50%,-50%) scale(60);
opacity: 0;
}
}
@theme {
--color-tch29-bg: #07101a;
--color-tch29-bg: oklch(0.15 0.02 245);
--color-tch29-ink: #dceaf7;
--color-tch29-ink: oklch(0.92 0.02 240);
--color-tch29-body: #8fa6bb;
--color-tch29-body: oklch(0.7 0.03 240);
--color-tch29-ring-a: #50c8ff;
--color-tch29-ring-a: oklch(0.79 0.13 235);
--color-tch29-ring-b: #a05aff;
--color-tch29-ring-b: oklch(0.6 0.23 295);
--font-tch29-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tch29-display: 'Chakra Petch', system-ui, sans-serif;
--animate-tch29-ring-a: tch29-ring 0.9s cubic-bezier(0.2,0.6,0.3,1) forwards;
--animate-tch29-ring-b: tch29-ring 1.1s cubic-bezier(0.2,0.6,0.3,1) 0.12s forwards;
}
.tch-29 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@keyframes tch29-ring {
0% {
transform: translate(-50%,-50%) scale(0);
opacity: 0.9;
}
100% {
transform: translate(-50%,-50%) scale(60);
opacity: 0;
}
}
@theme {
--color-tch29-bg: #07101a;
--color-tch29-bg: oklch(0.15 0.02 245);
--color-tch29-ink: #dceaf7;
--color-tch29-ink: oklch(0.92 0.02 240);
--color-tch29-body: #8fa6bb;
--color-tch29-body: oklch(0.7 0.03 240);
--color-tch29-ring-a: #50c8ff;
--color-tch29-ring-a: oklch(0.79 0.13 235);
--color-tch29-ring-b: #a05aff;
--color-tch29-ring-b: oklch(0.6 0.23 295);
--font-tch29-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tch29-display: 'Chakra Petch', system-ui, sans-serif;
--animate-tch29-ring-a: tch29-ring 0.9s cubic-bezier(0.2,0.6,0.3,1) forwards;
--animate-tch29-ring-b: tch29-ring 1.1s cubic-bezier(0.2,0.6,0.3,1) 0.12s forwards;
}
.tch-29 {
width: 100%;
min-height: 100svh;
}(() => {
const root = document.querySelector('.tch-29');
if (!root) return;
const card = root.querySelector('[data-tch29="root"]');
if (!card) return;
if (!matchMedia('(hover: hover) and (pointer: fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
card.addEventListener('pointerenter', (e) => {
const r = card.getBoundingClientRect();
card.style.setProperty('--tch29-x', '' + (e.clientX - r.left) + 'px');
card.style.setProperty('--tch29-y', '' + (e.clientY - r.top) + 'px');
});
})();(() => {
const root = document.querySelector('.tch-29');
if (!root) return;
const card = root.querySelector('[data-tch29="root"]');
if (!card) return;
if (!matchMedia('(hover: hover) and (pointer: fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
card.addEventListener('pointerenter', (e) => {
const r = card.getBoundingClientRect();
card.style.setProperty('--tch29-x', '' + (e.clientX - r.left) + 'px');
card.style.setProperty('--tch29-y', '' + (e.clientY - r.top) + 'px');
});
})();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";
@keyframes tch29-ring {
0% {
transform: translate(-50%,-50%) scale(0);
opacity: 0.9;
}
100% {
transform: translate(-50%,-50%) scale(60);
opacity: 0;
}
}
@theme {
--color-tch29-bg: #07101a;
--color-tch29-bg: oklch(0.15 0.02 245);
--color-tch29-ink: #dceaf7;
--color-tch29-ink: oklch(0.92 0.02 240);
--color-tch29-body: #8fa6bb;
--color-tch29-body: oklch(0.7 0.03 240);
--color-tch29-ring-a: #50c8ff;
--color-tch29-ring-a: oklch(0.79 0.13 235);
--color-tch29-ring-b: #a05aff;
--color-tch29-ring-b: oklch(0.6 0.23 295);
--font-tch29-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tch29-display: 'Chakra Petch', system-ui, sans-serif;
--animate-tch29-ring-a: tch29-ring 0.9s cubic-bezier(0.2,0.6,0.3,1) forwards;
--animate-tch29-ring-b: tch29-ring 1.1s cubic-bezier(0.2,0.6,0.3,1) 0.12s forwards;
}
.tch-29 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@keyframes tch29-ring {
0% {
transform: translate(-50%,-50%) scale(0);
opacity: 0.9;
}
100% {
transform: translate(-50%,-50%) scale(60);
opacity: 0;
}
}
@theme {
--color-tch29-bg: #07101a;
--color-tch29-bg: oklch(0.15 0.02 245);
--color-tch29-ink: #dceaf7;
--color-tch29-ink: oklch(0.92 0.02 240);
--color-tch29-body: #8fa6bb;
--color-tch29-body: oklch(0.7 0.03 240);
--color-tch29-ring-a: #50c8ff;
--color-tch29-ring-a: oklch(0.79 0.13 235);
--color-tch29-ring-b: #a05aff;
--color-tch29-ring-b: oklch(0.6 0.23 295);
--font-tch29-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tch29-display: 'Chakra Petch', system-ui, sans-serif;
--animate-tch29-ring-a: tch29-ring 0.9s cubic-bezier(0.2,0.6,0.3,1) forwards;
--animate-tch29-ring-b: tch29-ring 1.1s cubic-bezier(0.2,0.6,0.3,1) 0.12s forwards;
}
.tch-29 {
width: 100%;
min-height: 100svh;
}<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=Chakra+Petch:wght@500;700&family=Space+Grotesk:wght@400;500&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=Chakra+Petch:wght@500;700&family=Space+Grotesk:wght@400;500&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Two pseudo-elements instead of spawned nodes removes the whole cleanup problem the CSS original's gotcha list warned about — and caps the effect at two rings, which is the honest trade.
- Positioning a pseudo-element from custom properties (
before:top-[var(--tch29-y,50%)]) is the utility form of cursor anchoring, and the fallback value in thevar()means the card looks right before any pointer has arrived. - UI change from the CSS original: the CTA is a real link with a 44px target and a focus ring, and the handler is guarded on pointer type and reduced motion.
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 Cursor-Anchored Shockwave Ripple Card
Source: https://codefronts.com/motion/tailwind-css-card-hover-effects/tailwind-cursor-anchored-shockwave-card/
Two concentric rings fire outward from the exact point the cursor enters the card, the second trailing the first by 120ms. The anchoring is the whole trick: a pointerenter handler writes the entry point into two custom properties, and the rings' origin reads them.
## HTML
```html
<section class="tch-29 w-full min-h-svh grid place-items-center bg-tch29-bg font-tch29-sans p-8">
<article data-tch29="root" class="group relative w-[340px] h-[440px] rounded-[18px] overflow-hidden cursor-pointer bg-[radial-gradient(circle_at_50%_40%,#0e2236,#060d16)] border border-tch29-ring-a/20
before:content-[''] before:absolute before:z-1 before:size-2.5 before:rounded-full before:pointer-events-none before:top-[var(--tch29-y,50%)] before:left-[var(--tch29-x,50%)] before:-translate-x-1/2 before:-translate-y-1/2 before:scale-0 before:border-2 before:border-tch29-ring-a/70 before:shadow-[0_0_30px_var(--color-tch29-ring-a)] group-hover:before:animate-tch29-ring-a motion-reduce:group-hover:before:animate-none
after:content-[''] after:absolute after:z-1 after:size-2.5 after:rounded-full after:pointer-events-none after:top-[var(--tch29-y,50%)] after:left-[var(--tch29-x,50%)] after:-translate-x-1/2 after:-translate-y-1/2 after:scale-0 after:border after:border-tch29-ring-b/60 group-hover:after:animate-tch29-ring-b motion-reduce:group-hover:after:animate-none">
<div class="relative z-2 h-full p-[2.4rem] flex flex-col justify-between text-tch29-ink">
<span class="font-tch29-display text-[0.72rem] tracking-[0.24em] uppercase text-tch29-ring-a">// Energy Core</span>
<h2 class="font-tch29-display font-bold text-[2.6rem] leading-[0.95]">Pulse<br>Reactor</h2>
<p class="text-[0.86rem] leading-[1.6] text-tch29-body">Every interaction sends a wave outward. Hover anywhere — the ripple starts where you enter.</p>
<a href="#" class="self-start inline-flex items-center min-h-11 px-6 rounded-lg font-tch29-display text-[0.85rem] tracking-[0.06em] no-underline bg-tch29-ring-a/12 border border-tch29-ring-a/40 text-tch29-ring-a transition-colors duration-200 hover:bg-tch29-ring-a/22 focus-visible:outline-2 focus-visible:outline-tch29-ring-a focus-visible:outline-offset-2">Discharge ⚡</a>
</div>
</article>
</section>
```
## CSS
```css
@import "tailwindcss";
@keyframes tch29-ring {
0% {
transform: translate(-50%,-50%) scale(0);
opacity: 0.9;
}
100% {
transform: translate(-50%,-50%) scale(60);
opacity: 0;
}
}
@theme {
--color-tch29-bg: #07101a;
--color-tch29-bg: oklch(0.15 0.02 245);
--color-tch29-ink: #dceaf7;
--color-tch29-ink: oklch(0.92 0.02 240);
--color-tch29-body: #8fa6bb;
--color-tch29-body: oklch(0.7 0.03 240);
--color-tch29-ring-a: #50c8ff;
--color-tch29-ring-a: oklch(0.79 0.13 235);
--color-tch29-ring-b: #a05aff;
--color-tch29-ring-b: oklch(0.6 0.23 295);
--font-tch29-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tch29-display: 'Chakra Petch', system-ui, sans-serif;
--animate-tch29-ring-a: tch29-ring 0.9s cubic-bezier(0.2,0.6,0.3,1) forwards;
--animate-tch29-ring-b: tch29-ring 1.1s cubic-bezier(0.2,0.6,0.3,1) 0.12s forwards;
}
.tch-29 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.tch-29');
if (!root) return;
const card = root.querySelector('[data-tch29="root"]');
if (!card) return;
if (!matchMedia('(hover: hover) and (pointer: fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
card.addEventListener('pointerenter', (e) => {
const r = card.getBoundingClientRect();
card.style.setProperty('--tch29-x', '' + (e.clientX - r.left) + 'px');
card.style.setProperty('--tch29-y', '' + (e.clientY - r.top) + 'px');
});
})();
```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 Cursor-Anchored Shockwave Ripple Card
Source: https://codefronts.com/motion/tailwind-css-card-hover-effects/tailwind-cursor-anchored-shockwave-card/
Two concentric rings fire outward from the exact point the cursor enters the card, the second trailing the first by 120ms. The anchoring is the whole trick: a pointerenter handler writes the entry point into two custom properties, and the rings' origin reads them.
## HTML
```html
<section class="tch-29 w-full min-h-svh grid place-items-center bg-tch29-bg font-tch29-sans p-8">
<article data-tch29="root" class="group relative w-[340px] h-[440px] rounded-[18px] overflow-hidden cursor-pointer bg-[radial-gradient(circle_at_50%_40%,#0e2236,#060d16)] border border-tch29-ring-a/20
before:content-[''] before:absolute before:z-1 before:size-2.5 before:rounded-full before:pointer-events-none before:top-[var(--tch29-y,50%)] before:left-[var(--tch29-x,50%)] before:-translate-x-1/2 before:-translate-y-1/2 before:scale-0 before:border-2 before:border-tch29-ring-a/70 before:shadow-[0_0_30px_var(--color-tch29-ring-a)] group-hover:before:animate-tch29-ring-a motion-reduce:group-hover:before:animate-none
after:content-[''] after:absolute after:z-1 after:size-2.5 after:rounded-full after:pointer-events-none after:top-[var(--tch29-y,50%)] after:left-[var(--tch29-x,50%)] after:-translate-x-1/2 after:-translate-y-1/2 after:scale-0 after:border after:border-tch29-ring-b/60 group-hover:after:animate-tch29-ring-b motion-reduce:group-hover:after:animate-none">
<div class="relative z-2 h-full p-[2.4rem] flex flex-col justify-between text-tch29-ink">
<span class="font-tch29-display text-[0.72rem] tracking-[0.24em] uppercase text-tch29-ring-a">// Energy Core</span>
<h2 class="font-tch29-display font-bold text-[2.6rem] leading-[0.95]">Pulse<br>Reactor</h2>
<p class="text-[0.86rem] leading-[1.6] text-tch29-body">Every interaction sends a wave outward. Hover anywhere — the ripple starts where you enter.</p>
<a href="#" class="self-start inline-flex items-center min-h-11 px-6 rounded-lg font-tch29-display text-[0.85rem] tracking-[0.06em] no-underline bg-tch29-ring-a/12 border border-tch29-ring-a/40 text-tch29-ring-a transition-colors duration-200 hover:bg-tch29-ring-a/22 focus-visible:outline-2 focus-visible:outline-tch29-ring-a focus-visible:outline-offset-2">Discharge ⚡</a>
</div>
</article>
</section>
```
## CSS
```css
@import "tailwindcss";
@keyframes tch29-ring {
0% {
transform: translate(-50%,-50%) scale(0);
opacity: 0.9;
}
100% {
transform: translate(-50%,-50%) scale(60);
opacity: 0;
}
}
@theme {
--color-tch29-bg: #07101a;
--color-tch29-bg: oklch(0.15 0.02 245);
--color-tch29-ink: #dceaf7;
--color-tch29-ink: oklch(0.92 0.02 240);
--color-tch29-body: #8fa6bb;
--color-tch29-body: oklch(0.7 0.03 240);
--color-tch29-ring-a: #50c8ff;
--color-tch29-ring-a: oklch(0.79 0.13 235);
--color-tch29-ring-b: #a05aff;
--color-tch29-ring-b: oklch(0.6 0.23 295);
--font-tch29-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tch29-display: 'Chakra Petch', system-ui, sans-serif;
--animate-tch29-ring-a: tch29-ring 0.9s cubic-bezier(0.2,0.6,0.3,1) forwards;
--animate-tch29-ring-b: tch29-ring 1.1s cubic-bezier(0.2,0.6,0.3,1) 0.12s forwards;
}
.tch-29 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.tch-29');
if (!root) return;
const card = root.querySelector('[data-tch29="root"]');
if (!card) return;
if (!matchMedia('(hover: hover) and (pointer: fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
card.addEventListener('pointerenter', (e) => {
const r = card.getBoundingClientRect();
card.style.setProperty('--tch29-x', '' + (e.clientX - r.left) + 'px');
card.style.setProperty('--tch29-y', '' + (e.clientY - r.top) + 'px');
});
})();
```How this works
The anchor is what makes it feel physical. A ripple from the card's centre reads as a canned animation. A ripple from wherever your cursor crossed the edge reads as impact. That difference costs one pointerenter handler and two custom properties.
Pseudo-elements instead of spawned nodes. The common implementation appends a div per ripple and removes it on animationend; forget the cleanup and a hovered card accumulates hundreds of dead nodes. Two pseudo-elements re-run their animation on each hover with nothing to garbage-collect.
The stagger is in the token. --animate-tch29-ring-b carries its own 0.12s delay and a slightly longer duration, so the second wave is visibly behind and slightly softer — propagation, not a double ring.
Make it yours
- Ring count: add a third via a nested element if two waves are not enough — pseudo-elements cap you at two per box.
- Impact strength: the
scale(60)target and the duration. Bigger and faster reads as a blast. - Ring colours:
--color-tch29-ring-aand-b; cyan into violet reads as energy discharge.
Gotchas — read before shipping
- Use
pointerenter.mouseoverfires again on every child boundary and re-triggers the wave mid-card. - Rings need
pointer-events-none— atscale(60)one covers the entire card. - Skip the effect under
prefers-reduced-motion; a large expanding ring is exactly the vestibular trigger the preference names. - Keep the rings behind the content (
z-1vsz-2), or the CTA flickers under each wave.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
PointerEvent, custom properties and transform animations are universally supported; the floor is Tailwind v4's generated sheet.