11 / 20
All 20 in 20 Tailwind CSS Animated Buttons
- 01Tailwind Neon Glow Button with Pulsing Halo
- 02Tailwind Background Slide Hover Button (Four Directions)
- 03Tailwind Ripple Effect Button with No JavaScript
- 04Tailwind Animated Border Draw Button with Staged Delays
- 05Tailwind 3D Press Effect Button with Keycap Depth
- 06Tailwind Liquid Fill Button with Animated Wave Crest
- 07Tailwind Moving Gradient CTA Button
- 08Tailwind Shine Sweep Hover Button with Gloss Streak
- 09Tailwind Jelly Bounce Button with Elastic Squash
- 10Tailwind Text Flip Button Hover with 3D Rotation
- 11Tailwind Animated CTA Hero Button with Beacon Rings & Particle Burst
- 12Tailwind Submit Button Success State with Checkmark Draw
- 13Tailwind Animated Download Button with Progress Fill
- 14Tailwind Hamburger to Close Button with aria-expanded Variants
- 15Tailwind Add to Cart Button with Flying Product Dot
- 16Tailwind Social Share Buttons with Brand Hover & Copy Link
- 17Tailwind Animated Toggle Switch with peer-checked & has-checked
- 18Tailwind Loading Spinner Inside Button with State Machine
- 19Tailwind Next & Prev Arrow Buttons with Chasing Icons
- 20Tailwind Ghost Button with Cursor-Aware Background Reveal
Tailwind Animated CTA Hero Button with Beacon Rings & Particle Burst
A landing-page CTA with two radar rings pinging outward and a particle burst on click. The rings are pure utilities on one wrapper's two pseudo-elements; the JS does nothing but stamp a vector onto each particle and let the animation system handle every frame.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tab-11 w-full min-h-svh grid place-items-center bg-tab11-bg font-tab11-sans px-6 py-10">
<div class="w-[min(520px,100%)] text-center">
<p class="text-[0.8rem] tracking-[0.16em] uppercase text-tab11-beacon m-0">✦ Now in public beta</p>
<h1 class="mt-4 mb-4 text-[clamp(2rem,5vw,3rem)] font-bold leading-[1.1] text-white">The dev platform<br>built for speed.</h1>
<p class="mb-8 text-base leading-relaxed text-tab11-mut">Deploy anywhere. Scale effortlessly. Ship today.</p>
<span id="tab11-wrap" class="relative inline-block mb-4 before:content-[''] before:absolute before:-inset-1.5 before:rounded-2xl before:border-2 before:border-tab11-beacon before:opacity-0 before:animate-tab11-beacon before:pointer-events-none after:content-[''] after:absolute after:-inset-1.5 after:rounded-2xl after:border-2 after:border-tab11-beacon after:opacity-0 after:animate-tab11-beacon after:[animation-delay:0.7s] after:pointer-events-none motion-reduce:before:animate-none motion-reduce:after:animate-none">
<button type="button" id="tab11-btn" class="relative z-10 min-h-11 px-10 py-3.5 text-[1.05rem] font-bold tracking-[0.03em] text-white rounded-xl cursor-pointer bg-[linear-gradient(135deg,#7c3aed,#2563eb)] shadow-[0_4px_24px_oklch(0.53_0.25_295/0.45)] transition-[translate,filter] duration-150 hover:brightness-110 hover:-translate-y-0.5 active:translate-y-0 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white motion-reduce:transition-none">Start Building Free</button>
</span>
<p class="text-[0.75rem] text-tab11-dim">No credit card · 50k free API calls included</p>
</div>
</section><section class="tab-11 w-full min-h-svh grid place-items-center bg-tab11-bg font-tab11-sans px-6 py-10">
<div class="w-[min(520px,100%)] text-center">
<p class="text-[0.8rem] tracking-[0.16em] uppercase text-tab11-beacon m-0">✦ Now in public beta</p>
<h1 class="mt-4 mb-4 text-[clamp(2rem,5vw,3rem)] font-bold leading-[1.1] text-white">The dev platform<br>built for speed.</h1>
<p class="mb-8 text-base leading-relaxed text-tab11-mut">Deploy anywhere. Scale effortlessly. Ship today.</p>
<span id="tab11-wrap" class="relative inline-block mb-4 before:content-[''] before:absolute before:-inset-1.5 before:rounded-2xl before:border-2 before:border-tab11-beacon before:opacity-0 before:animate-tab11-beacon before:pointer-events-none after:content-[''] after:absolute after:-inset-1.5 after:rounded-2xl after:border-2 after:border-tab11-beacon after:opacity-0 after:animate-tab11-beacon after:[animation-delay:0.7s] after:pointer-events-none motion-reduce:before:animate-none motion-reduce:after:animate-none">
<button type="button" id="tab11-btn" class="relative z-10 min-h-11 px-10 py-3.5 text-[1.05rem] font-bold tracking-[0.03em] text-white rounded-xl cursor-pointer bg-[linear-gradient(135deg,#7c3aed,#2563eb)] shadow-[0_4px_24px_oklch(0.53_0.25_295/0.45)] transition-[translate,filter] duration-150 hover:brightness-110 hover:-translate-y-0.5 active:translate-y-0 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white motion-reduce:transition-none">Start Building Free</button>
</span>
<p class="text-[0.75rem] text-tab11-dim">No credit card · 50k free API calls included</p>
</div>
</section>@import "tailwindcss";
@theme {
--color-tab11-bg: #050510;
--color-tab11-bg: oklch(0.07 0.02 280);
--color-tab11-beacon: #a78bfa;
--color-tab11-beacon: oklch(0.74 0.15 292);
--color-tab11-mut: #94a3b8;
--color-tab11-mut: oklch(0.72 0.03 255);
--color-tab11-dim: #64748b;
--color-tab11-dim: oklch(0.55 0.03 255);
--font-tab11-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab11-beacon: tab11-beacon 2s ease-out infinite;
--animate-tab11-burst: tab11-burst 0.7s ease-out forwards;
}
@keyframes tab11-beacon {
0% {
scale: 1;
opacity: 0.7;
}
100% {
scale: 1.5;
opacity: 0;
}
}
@keyframes tab11-burst {
0% {
translate: -50% -50%;
scale: 1;
opacity: 1;
}
100% {
translate: calc(-50% + var(--dx)) calc(-50% + var(--dy));
scale: 0;
opacity: 0;
}
}
.tab-11 {
width: 100%;
min-height: 100svh;
}
.tab-11 ::selection {
background: var(--color-tab11-beacon);
color: #14091f;
}@import "tailwindcss";
@theme {
--color-tab11-bg: #050510;
--color-tab11-bg: oklch(0.07 0.02 280);
--color-tab11-beacon: #a78bfa;
--color-tab11-beacon: oklch(0.74 0.15 292);
--color-tab11-mut: #94a3b8;
--color-tab11-mut: oklch(0.72 0.03 255);
--color-tab11-dim: #64748b;
--color-tab11-dim: oklch(0.55 0.03 255);
--font-tab11-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab11-beacon: tab11-beacon 2s ease-out infinite;
--animate-tab11-burst: tab11-burst 0.7s ease-out forwards;
}
@keyframes tab11-beacon {
0% {
scale: 1;
opacity: 0.7;
}
100% {
scale: 1.5;
opacity: 0;
}
}
@keyframes tab11-burst {
0% {
translate: -50% -50%;
scale: 1;
opacity: 1;
}
100% {
translate: calc(-50% + var(--dx)) calc(-50% + var(--dy));
scale: 0;
opacity: 0;
}
}
.tab-11 {
width: 100%;
min-height: 100svh;
}
.tab-11 ::selection {
background: var(--color-tab11-beacon);
color: #14091f;
}(function () {
var wrap = document.getElementById('tab11-wrap');
var btn = document.getElementById('tab11-btn');
if (!btn || !wrap) return;
var COLOURS = ['#a78bfa', '#60a5fa', '#34d399', '#f472b6', '#fbbf24', '#ffffff'];
var COUNT = 12;
var calm = window.matchMedia('(prefers-reduced-motion: reduce)');
btn.addEventListener('click', function (e) {
if (calm.matches) return;
for (var i = 0; i < COUNT; i++) {
var p = document.createElement('span');
p.className = 'absolute top-0 left-0 size-2 rounded-full pointer-events-none animate-tab11-burst will-change-transform';
var angle = Math.random() * Math.PI * 2;
var dist = 40 + Math.random() * 40;
p.style.setProperty('--dx', Math.cos(angle) * dist + 'px');
p.style.setProperty('--dy', Math.sin(angle) * dist + 'px');
p.style.background = COLOURS[Math.floor(Math.random() * COLOURS.length)];
p.style.left = e.offsetX + 'px';
p.style.top = e.offsetY + 'px';
wrap.appendChild(p);
p.addEventListener('animationend', function () { this.remove(); });
}
});
})();(function () {
var wrap = document.getElementById('tab11-wrap');
var btn = document.getElementById('tab11-btn');
if (!btn || !wrap) return;
var COLOURS = ['#a78bfa', '#60a5fa', '#34d399', '#f472b6', '#fbbf24', '#ffffff'];
var COUNT = 12;
var calm = window.matchMedia('(prefers-reduced-motion: reduce)');
btn.addEventListener('click', function (e) {
if (calm.matches) return;
for (var i = 0; i < COUNT; i++) {
var p = document.createElement('span');
p.className = 'absolute top-0 left-0 size-2 rounded-full pointer-events-none animate-tab11-burst will-change-transform';
var angle = Math.random() * Math.PI * 2;
var dist = 40 + Math.random() * 40;
p.style.setProperty('--dx', Math.cos(angle) * dist + 'px');
p.style.setProperty('--dy', Math.sin(angle) * dist + 'px');
p.style.background = COLOURS[Math.floor(Math.random() * COLOURS.length)];
p.style.left = e.offsetX + 'px';
p.style.top = e.offsetY + 'px';
wrap.appendChild(p);
p.addEventListener('animationend', function () { this.remove(); });
}
});
})();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-tab11-bg: #050510;
--color-tab11-bg: oklch(0.07 0.02 280);
--color-tab11-beacon: #a78bfa;
--color-tab11-beacon: oklch(0.74 0.15 292);
--color-tab11-mut: #94a3b8;
--color-tab11-mut: oklch(0.72 0.03 255);
--color-tab11-dim: #64748b;
--color-tab11-dim: oklch(0.55 0.03 255);
--font-tab11-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab11-beacon: tab11-beacon 2s ease-out infinite;
--animate-tab11-burst: tab11-burst 0.7s ease-out forwards;
}
@keyframes tab11-beacon {
0% {
scale: 1;
opacity: 0.7;
}
100% {
scale: 1.5;
opacity: 0;
}
}
@keyframes tab11-burst {
0% {
translate: -50% -50%;
scale: 1;
opacity: 1;
}
100% {
translate: calc(-50% + var(--dx)) calc(-50% + var(--dy));
scale: 0;
opacity: 0;
}
}
.tab-11 {
width: 100%;
min-height: 100svh;
}
.tab-11 ::selection {
background: var(--color-tab11-beacon);
color: #14091f;
}@import "tailwindcss";
@theme {
--color-tab11-bg: #050510;
--color-tab11-bg: oklch(0.07 0.02 280);
--color-tab11-beacon: #a78bfa;
--color-tab11-beacon: oklch(0.74 0.15 292);
--color-tab11-mut: #94a3b8;
--color-tab11-mut: oklch(0.72 0.03 255);
--color-tab11-dim: #64748b;
--color-tab11-dim: oklch(0.55 0.03 255);
--font-tab11-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab11-beacon: tab11-beacon 2s ease-out infinite;
--animate-tab11-burst: tab11-burst 0.7s ease-out forwards;
}
@keyframes tab11-beacon {
0% {
scale: 1;
opacity: 0.7;
}
100% {
scale: 1.5;
opacity: 0;
}
}
@keyframes tab11-burst {
0% {
translate: -50% -50%;
scale: 1;
opacity: 1;
}
100% {
translate: calc(-50% + var(--dx)) calc(-50% + var(--dy));
scale: 0;
opacity: 0;
}
}
.tab-11 {
width: 100%;
min-height: 100svh;
}
.tab-11 ::selection {
background: var(--color-tab11-beacon);
color: #14091f;
}<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">
<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.
- The particle class list is assembled in JS as utilities, not as a CSS class — which is only safe because the browser build compiles at runtime. Under a CLI build these strings must be safelisted or the utilities never get generated. That is the one real cost of writing utilities from JavaScript.
after:[animation-delay:0.7s]is the whole second ring. The CSS version needed a dedicated::afterrule to say it.- UI change from the CSS original: the controller now respects
prefers-reduced-motionin JS as well as CSS, so the burst does not spawn twelve invisible nodes on every click for users who asked for calm.
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 Animated CTA Hero Button with Beacon Rings & Particle Burst
Source: https://codefronts.com/motion/tailwind-css-animated-buttons/tailwind-animated-cta-hero-button/
A landing-page CTA with two radar rings pinging outward and a particle burst on click. The rings are pure utilities on one wrapper's two pseudo-elements; the JS does nothing but stamp a vector onto each particle and let the animation system handle every frame.
## HTML
```html
<section class="tab-11 w-full min-h-svh grid place-items-center bg-tab11-bg font-tab11-sans px-6 py-10">
<div class="w-[min(520px,100%)] text-center">
<p class="text-[0.8rem] tracking-[0.16em] uppercase text-tab11-beacon m-0">✦ Now in public beta</p>
<h1 class="mt-4 mb-4 text-[clamp(2rem,5vw,3rem)] font-bold leading-[1.1] text-white">The dev platform<br>built for speed.</h1>
<p class="mb-8 text-base leading-relaxed text-tab11-mut">Deploy anywhere. Scale effortlessly. Ship today.</p>
<span id="tab11-wrap" class="relative inline-block mb-4 before:content-[''] before:absolute before:-inset-1.5 before:rounded-2xl before:border-2 before:border-tab11-beacon before:opacity-0 before:animate-tab11-beacon before:pointer-events-none after:content-[''] after:absolute after:-inset-1.5 after:rounded-2xl after:border-2 after:border-tab11-beacon after:opacity-0 after:animate-tab11-beacon after:[animation-delay:0.7s] after:pointer-events-none motion-reduce:before:animate-none motion-reduce:after:animate-none">
<button type="button" id="tab11-btn" class="relative z-10 min-h-11 px-10 py-3.5 text-[1.05rem] font-bold tracking-[0.03em] text-white rounded-xl cursor-pointer bg-[linear-gradient(135deg,#7c3aed,#2563eb)] shadow-[0_4px_24px_oklch(0.53_0.25_295/0.45)] transition-[translate,filter] duration-150 hover:brightness-110 hover:-translate-y-0.5 active:translate-y-0 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white motion-reduce:transition-none">Start Building Free</button>
</span>
<p class="text-[0.75rem] text-tab11-dim">No credit card · 50k free API calls included</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tab11-bg: #050510;
--color-tab11-bg: oklch(0.07 0.02 280);
--color-tab11-beacon: #a78bfa;
--color-tab11-beacon: oklch(0.74 0.15 292);
--color-tab11-mut: #94a3b8;
--color-tab11-mut: oklch(0.72 0.03 255);
--color-tab11-dim: #64748b;
--color-tab11-dim: oklch(0.55 0.03 255);
--font-tab11-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab11-beacon: tab11-beacon 2s ease-out infinite;
--animate-tab11-burst: tab11-burst 0.7s ease-out forwards;
}
@keyframes tab11-beacon {
0% {
scale: 1;
opacity: 0.7;
}
100% {
scale: 1.5;
opacity: 0;
}
}
@keyframes tab11-burst {
0% {
translate: -50% -50%;
scale: 1;
opacity: 1;
}
100% {
translate: calc(-50% + var(--dx)) calc(-50% + var(--dy));
scale: 0;
opacity: 0;
}
}
.tab-11 {
width: 100%;
min-height: 100svh;
}
.tab-11 ::selection {
background: var(--color-tab11-beacon);
color: #14091f;
}
```
## JavaScript
```js
(function () {
var wrap = document.getElementById('tab11-wrap');
var btn = document.getElementById('tab11-btn');
if (!btn || !wrap) return;
var COLOURS = ['#a78bfa', '#60a5fa', '#34d399', '#f472b6', '#fbbf24', '#ffffff'];
var COUNT = 12;
var calm = window.matchMedia('(prefers-reduced-motion: reduce)');
btn.addEventListener('click', function (e) {
if (calm.matches) return;
for (var i = 0; i < COUNT; i++) {
var p = document.createElement('span');
p.className = 'absolute top-0 left-0 size-2 rounded-full pointer-events-none animate-tab11-burst will-change-transform';
var angle = Math.random() * Math.PI * 2;
var dist = 40 + Math.random() * 40;
p.style.setProperty('--dx', Math.cos(angle) * dist + 'px');
p.style.setProperty('--dy', Math.sin(angle) * dist + 'px');
p.style.background = COLOURS[Math.floor(Math.random() * COLOURS.length)];
p.style.left = e.offsetX + 'px';
p.style.top = e.offsetY + 'px';
wrap.appendChild(p);
p.addEventListener('animationend', function () { this.remove(); });
}
});
})();
```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 Animated CTA Hero Button with Beacon Rings & Particle Burst
Source: https://codefronts.com/motion/tailwind-css-animated-buttons/tailwind-animated-cta-hero-button/
A landing-page CTA with two radar rings pinging outward and a particle burst on click. The rings are pure utilities on one wrapper's two pseudo-elements; the JS does nothing but stamp a vector onto each particle and let the animation system handle every frame.
## HTML
```html
<section class="tab-11 w-full min-h-svh grid place-items-center bg-tab11-bg font-tab11-sans px-6 py-10">
<div class="w-[min(520px,100%)] text-center">
<p class="text-[0.8rem] tracking-[0.16em] uppercase text-tab11-beacon m-0">✦ Now in public beta</p>
<h1 class="mt-4 mb-4 text-[clamp(2rem,5vw,3rem)] font-bold leading-[1.1] text-white">The dev platform<br>built for speed.</h1>
<p class="mb-8 text-base leading-relaxed text-tab11-mut">Deploy anywhere. Scale effortlessly. Ship today.</p>
<span id="tab11-wrap" class="relative inline-block mb-4 before:content-[''] before:absolute before:-inset-1.5 before:rounded-2xl before:border-2 before:border-tab11-beacon before:opacity-0 before:animate-tab11-beacon before:pointer-events-none after:content-[''] after:absolute after:-inset-1.5 after:rounded-2xl after:border-2 after:border-tab11-beacon after:opacity-0 after:animate-tab11-beacon after:[animation-delay:0.7s] after:pointer-events-none motion-reduce:before:animate-none motion-reduce:after:animate-none">
<button type="button" id="tab11-btn" class="relative z-10 min-h-11 px-10 py-3.5 text-[1.05rem] font-bold tracking-[0.03em] text-white rounded-xl cursor-pointer bg-[linear-gradient(135deg,#7c3aed,#2563eb)] shadow-[0_4px_24px_oklch(0.53_0.25_295/0.45)] transition-[translate,filter] duration-150 hover:brightness-110 hover:-translate-y-0.5 active:translate-y-0 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white motion-reduce:transition-none">Start Building Free</button>
</span>
<p class="text-[0.75rem] text-tab11-dim">No credit card · 50k free API calls included</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tab11-bg: #050510;
--color-tab11-bg: oklch(0.07 0.02 280);
--color-tab11-beacon: #a78bfa;
--color-tab11-beacon: oklch(0.74 0.15 292);
--color-tab11-mut: #94a3b8;
--color-tab11-mut: oklch(0.72 0.03 255);
--color-tab11-dim: #64748b;
--color-tab11-dim: oklch(0.55 0.03 255);
--font-tab11-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab11-beacon: tab11-beacon 2s ease-out infinite;
--animate-tab11-burst: tab11-burst 0.7s ease-out forwards;
}
@keyframes tab11-beacon {
0% {
scale: 1;
opacity: 0.7;
}
100% {
scale: 1.5;
opacity: 0;
}
}
@keyframes tab11-burst {
0% {
translate: -50% -50%;
scale: 1;
opacity: 1;
}
100% {
translate: calc(-50% + var(--dx)) calc(-50% + var(--dy));
scale: 0;
opacity: 0;
}
}
.tab-11 {
width: 100%;
min-height: 100svh;
}
.tab-11 ::selection {
background: var(--color-tab11-beacon);
color: #14091f;
}
```
## JavaScript
```js
(function () {
var wrap = document.getElementById('tab11-wrap');
var btn = document.getElementById('tab11-btn');
if (!btn || !wrap) return;
var COLOURS = ['#a78bfa', '#60a5fa', '#34d399', '#f472b6', '#fbbf24', '#ffffff'];
var COUNT = 12;
var calm = window.matchMedia('(prefers-reduced-motion: reduce)');
btn.addEventListener('click', function (e) {
if (calm.matches) return;
for (var i = 0; i < COUNT; i++) {
var p = document.createElement('span');
p.className = 'absolute top-0 left-0 size-2 rounded-full pointer-events-none animate-tab11-burst will-change-transform';
var angle = Math.random() * Math.PI * 2;
var dist = 40 + Math.random() * 40;
p.style.setProperty('--dx', Math.cos(angle) * dist + 'px');
p.style.setProperty('--dy', Math.sin(angle) * dist + 'px');
p.style.background = COLOURS[Math.floor(Math.random() * COLOURS.length)];
p.style.left = e.offsetX + 'px';
p.style.top = e.offsetY + 'px';
wrap.appendChild(p);
p.addEventListener('animationend', function () { this.remove(); });
}
});
})();
```How this works
The rings are a wrapper, not the button. The button needs overflow to stay visible and a solid fill; the pings need to escape its bounds. So the wrapper carries before: and after: at -inset-1.5, each running the same scale-and-fade keyframe, the second delayed 0.7s. Two rings from one timeline.
JS writes data, CSS animates. Each click appends twelve spans; each gets a random angle and distance baked into --dx/--dy and a colour. The keyframe interpolates to translate(var(--dx),var(--dy)) scale(0). There is no requestAnimationFrame loop anywhere — the browser owns timing, which is why the burst stays smooth while the rings are also running.
Reduced motion is honoured in both layers. The rings stop via motion-reduce:, and the controller checks matchMedia('(prefers-reduced-motion: reduce)') before spawning particles — CSS alone cannot prevent the DOM churn.
Make it yours
- Ring colour:
--color-tab11-beacondrives both pseudo-elements. - Particle count: the
COUNTconstant (12). Past ~20 you will see frame drops on low-end phones. - Ring cadence lives in the token:
--animate-tab11-beacon: tab11-beacon 2s ease-out infinite.
Gotchas — read before shipping
- Particles need
pointer-events-none, or a mid-burst second click lands on a particle instead of the button. - The wrapper must be
relativeandinline-block; particles are positioned against it, and a block wrapper stretches the ring rectangle to the full column width. - Spawning particles on every click without the
animationendcleanup leaks nodes — a hundred clicks is twelve hundred orphaned spans.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Custom properties in keyframes and CSS animations are universally supported; the floor is Tailwind v4's generated sheet.