19 / 28
All 28 in 28 Tailwind CSS Close Buttons
- 01Tailwind Modal Close Button (Top-Right Corner X)
- 02Accessible Tailwind Close Button with Focus Ring (WCAG)
- 03iOS-Style Tailwind Tab Close Button
- 04Tailwind Floating Chat Widget Close Button
- 05Tailwind Cookie Consent Banner Dismiss Button
- 06Tailwind Mobile Ad Banner Close Button (48px Tap Target)
- 07Neumorphic Tailwind Close Button (Soft UI)
- 08Tailwind Hamburger to X Menu Button Animation
- 09Tailwind Video Player Overlay Close Button
- 10Tailwind Drawer Close Button with Native Dialog
- 11Origami Fold Tailwind Close Button
- 12Lock Tumbler Tailwind Close Button (@property)
- 13Rotate on Hover Tailwind Close Button
- 14Magnetic Cursor Tailwind Close Button
- 15Liquid Goo Tailwind Close Button (SVG Filter)
- 16Glitch Effect Tailwind Close Button
- 17Trash Compactor Tailwind Close Button
- 18Material Ripple Tailwind Close Button
- 19Particle Burst Tailwind Close Button
- 20Spring Snap Tailwind Close Button
- 21Neon Glow Tailwind Close Button
- 22Corner Ribbon Tailwind Close Button
- 233D Fold Away Tailwind Close Button
- 24Hold to Confirm Tailwind Close Button
- 25Tailwind Toast Notification Dismiss Button
- 26Tailwind Lightbox Image Close Button
- 27Brutalist Tailwind Close Button
- 28Galaxy Vortex Tailwind Close Button
Particle Burst Tailwind Close Button
Eight particles from one 6px dot: a single keyframe animates eight box-shadow copies outward and shrinks them, while the × squashes and pops. The CSS original played it on :active, so a quick tap cut the burst off after a frame or two; the port triggers it with a data-burst attribute that the script clears on animationend, so every tap gets the full 550ms.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tcb-19 w-full min-h-svh grid place-items-center bg-[linear-gradient(180deg,var(--color-tcb19-bg),var(--color-tcb19-bg2))] px-6 py-12 font-tcb19-sans" aria-label="Particle burst close button demo">
<div class="grid w-full justify-items-center gap-7">
<button id="tcb-19-btn" type="button" aria-label="Close" class="group relative grid size-16 cursor-pointer place-items-center rounded-full border-[1.5px] border-tcb19-p1/40 bg-tcb19-btn shadow-[0_16px_36px_-18px_rgb(0_0_0/0.8)] transition-colors duration-200 hover:border-tcb19-p1 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-tcb19-p1 motion-reduce:transition-none">
<span aria-hidden="true" class="relative size-5 before:absolute before:top-1/2 before:left-1/2 before:h-[2.5px] before:w-5 before:-translate-1/2 before:rotate-45 before:rounded-full before:bg-tcb19-x after:absolute after:top-1/2 after:left-1/2 after:h-[2.5px] after:w-5 after:-translate-1/2 after:-rotate-45 after:rounded-full after:bg-tcb19-x group-data-burst:animate-tcb19-pop motion-reduce:animate-none!"></span>
<span data-burst-dot aria-hidden="true" class="pointer-events-none absolute top-1/2 left-1/2 -m-[3px] size-1.5 rounded-full group-data-burst:animate-tcb19-boom motion-reduce:animate-none!"></span>
</button>
<p aria-hidden="true" class="rounded-full px-3.5 py-2 font-tcb19-mono text-[11.5px] text-tcb19-mut ring-1 ring-white/12">8× box-shadow particles · group-data-burst: · animationend</p>
</div>
</section><section class="tcb-19 w-full min-h-svh grid place-items-center bg-[linear-gradient(180deg,var(--color-tcb19-bg),var(--color-tcb19-bg2))] px-6 py-12 font-tcb19-sans" aria-label="Particle burst close button demo">
<div class="grid w-full justify-items-center gap-7">
<button id="tcb-19-btn" type="button" aria-label="Close" class="group relative grid size-16 cursor-pointer place-items-center rounded-full border-[1.5px] border-tcb19-p1/40 bg-tcb19-btn shadow-[0_16px_36px_-18px_rgb(0_0_0/0.8)] transition-colors duration-200 hover:border-tcb19-p1 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-tcb19-p1 motion-reduce:transition-none">
<span aria-hidden="true" class="relative size-5 before:absolute before:top-1/2 before:left-1/2 before:h-[2.5px] before:w-5 before:-translate-1/2 before:rotate-45 before:rounded-full before:bg-tcb19-x after:absolute after:top-1/2 after:left-1/2 after:h-[2.5px] after:w-5 after:-translate-1/2 after:-rotate-45 after:rounded-full after:bg-tcb19-x group-data-burst:animate-tcb19-pop motion-reduce:animate-none!"></span>
<span data-burst-dot aria-hidden="true" class="pointer-events-none absolute top-1/2 left-1/2 -m-[3px] size-1.5 rounded-full group-data-burst:animate-tcb19-boom motion-reduce:animate-none!"></span>
</button>
<p aria-hidden="true" class="rounded-full px-3.5 py-2 font-tcb19-mono text-[11.5px] text-tcb19-mut ring-1 ring-white/12">8× box-shadow particles · group-data-burst: · animationend</p>
</div>
</section>@import "tailwindcss";
@theme {
--color-tcb19-bg: #211a2e;
--color-tcb19-bg: oklch(0.2 0.03 300);
--color-tcb19-bg2: #16121f;
--color-tcb19-bg2: oklch(0.15 0.02 290);
--color-tcb19-btn: #2a2138;
--color-tcb19-btn: oklch(0.24 0.035 300);
--color-tcb19-p1: #f0a23a;
--color-tcb19-p1: oklch(0.77 0.16 65);
--color-tcb19-p2: #e04aa6;
--color-tcb19-p2: oklch(0.65 0.22 340);
--color-tcb19-x: #f7efe6;
--color-tcb19-x: oklch(0.95 0.02 60);
--color-tcb19-mut: #bcaecb;
--color-tcb19-mut: oklch(0.77 0.04 300);
--font-tcb19-sans: 'Geist', system-ui, sans-serif;
--font-tcb19-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tcb19-boom: tcb19-boom 0.55s cubic-bezier(0.2,0.7,0.3,1) forwards;
--animate-tcb19-pop: tcb19-pop 0.55s cubic-bezier(0.34,1.56,0.64,1) forwards;
@keyframes tcb19-boom {
0% {
background: var(--color-tcb19-p1);
box-shadow: 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2);
}
60% {
background: transparent;
box-shadow: 22px 0 0 0 var(--color-tcb19-p1), 15px -15px 0 0 var(--color-tcb19-p2), 0 -22px 0 0 var(--color-tcb19-p1), -15px -15px 0 0 var(--color-tcb19-p2), -22px 0 0 0 var(--color-tcb19-p1), -15px 15px 0 0 var(--color-tcb19-p2), 0 22px 0 0 var(--color-tcb19-p1), 15px 15px 0 0 var(--color-tcb19-p2);
}
100% {
background: transparent;
box-shadow: 26px 0 0 -3px transparent, 18px -18px 0 -3px transparent, 0 -26px 0 -3px transparent, -18px -18px 0 -3px transparent, -26px 0 0 -3px transparent, -18px 18px 0 -3px transparent, 0 26px 0 -3px transparent, 18px 18px 0 -3px transparent;
}
}
@keyframes tcb19-pop {
0% {
transform: scale(1);
}
30% {
transform: scale(0.8);
}
70% {
transform: scale(1.12);
}
100% {
transform: scale(1);
}
}
}
/* 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. */
.tcb-19 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tcb19-bg: #211a2e;
--color-tcb19-bg: oklch(0.2 0.03 300);
--color-tcb19-bg2: #16121f;
--color-tcb19-bg2: oklch(0.15 0.02 290);
--color-tcb19-btn: #2a2138;
--color-tcb19-btn: oklch(0.24 0.035 300);
--color-tcb19-p1: #f0a23a;
--color-tcb19-p1: oklch(0.77 0.16 65);
--color-tcb19-p2: #e04aa6;
--color-tcb19-p2: oklch(0.65 0.22 340);
--color-tcb19-x: #f7efe6;
--color-tcb19-x: oklch(0.95 0.02 60);
--color-tcb19-mut: #bcaecb;
--color-tcb19-mut: oklch(0.77 0.04 300);
--font-tcb19-sans: 'Geist', system-ui, sans-serif;
--font-tcb19-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tcb19-boom: tcb19-boom 0.55s cubic-bezier(0.2,0.7,0.3,1) forwards;
--animate-tcb19-pop: tcb19-pop 0.55s cubic-bezier(0.34,1.56,0.64,1) forwards;
@keyframes tcb19-boom {
0% {
background: var(--color-tcb19-p1);
box-shadow: 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2);
}
60% {
background: transparent;
box-shadow: 22px 0 0 0 var(--color-tcb19-p1), 15px -15px 0 0 var(--color-tcb19-p2), 0 -22px 0 0 var(--color-tcb19-p1), -15px -15px 0 0 var(--color-tcb19-p2), -22px 0 0 0 var(--color-tcb19-p1), -15px 15px 0 0 var(--color-tcb19-p2), 0 22px 0 0 var(--color-tcb19-p1), 15px 15px 0 0 var(--color-tcb19-p2);
}
100% {
background: transparent;
box-shadow: 26px 0 0 -3px transparent, 18px -18px 0 -3px transparent, 0 -26px 0 -3px transparent, -18px -18px 0 -3px transparent, -26px 0 0 -3px transparent, -18px 18px 0 -3px transparent, 0 26px 0 -3px transparent, 18px 18px 0 -3px transparent;
}
}
@keyframes tcb19-pop {
0% {
transform: scale(1);
}
30% {
transform: scale(0.8);
}
70% {
transform: scale(1.12);
}
100% {
transform: scale(1);
}
}
}
/* 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. */
.tcb-19 {
width: 100%;
min-height: 100svh;
}(() => {
const btn = document.getElementById('tcb-19-btn');
if (!btn || btn.dataset.wired) return;
btn.dataset.wired = '1';
const dot = btn.querySelector('[data-burst-dot]');
const fire = () => {
btn.removeAttribute('data-burst');
requestAnimationFrame(() => requestAnimationFrame(() => btn.setAttribute('data-burst', '')));
};
btn.addEventListener('pointerdown', fire);
btn.addEventListener('keydown', (e) => { if ((e.key === 'Enter' || e.key === ' ') && !e.repeat) fire(); });
dot.addEventListener('animationend', () => btn.removeAttribute('data-burst'));
})();(() => {
const btn = document.getElementById('tcb-19-btn');
if (!btn || btn.dataset.wired) return;
btn.dataset.wired = '1';
const dot = btn.querySelector('[data-burst-dot]');
const fire = () => {
btn.removeAttribute('data-burst');
requestAnimationFrame(() => requestAnimationFrame(() => btn.setAttribute('data-burst', '')));
};
btn.addEventListener('pointerdown', fire);
btn.addEventListener('keydown', (e) => { if ((e.key === 'Enter' || e.key === ' ') && !e.repeat) fire(); });
dot.addEventListener('animationend', () => btn.removeAttribute('data-burst'));
})();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-tcb19-bg: #211a2e;
--color-tcb19-bg: oklch(0.2 0.03 300);
--color-tcb19-bg2: #16121f;
--color-tcb19-bg2: oklch(0.15 0.02 290);
--color-tcb19-btn: #2a2138;
--color-tcb19-btn: oklch(0.24 0.035 300);
--color-tcb19-p1: #f0a23a;
--color-tcb19-p1: oklch(0.77 0.16 65);
--color-tcb19-p2: #e04aa6;
--color-tcb19-p2: oklch(0.65 0.22 340);
--color-tcb19-x: #f7efe6;
--color-tcb19-x: oklch(0.95 0.02 60);
--color-tcb19-mut: #bcaecb;
--color-tcb19-mut: oklch(0.77 0.04 300);
--font-tcb19-sans: 'Geist', system-ui, sans-serif;
--font-tcb19-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tcb19-boom: tcb19-boom 0.55s cubic-bezier(0.2,0.7,0.3,1) forwards;
--animate-tcb19-pop: tcb19-pop 0.55s cubic-bezier(0.34,1.56,0.64,1) forwards;
@keyframes tcb19-boom {
0% {
background: var(--color-tcb19-p1);
box-shadow: 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2);
}
60% {
background: transparent;
box-shadow: 22px 0 0 0 var(--color-tcb19-p1), 15px -15px 0 0 var(--color-tcb19-p2), 0 -22px 0 0 var(--color-tcb19-p1), -15px -15px 0 0 var(--color-tcb19-p2), -22px 0 0 0 var(--color-tcb19-p1), -15px 15px 0 0 var(--color-tcb19-p2), 0 22px 0 0 var(--color-tcb19-p1), 15px 15px 0 0 var(--color-tcb19-p2);
}
100% {
background: transparent;
box-shadow: 26px 0 0 -3px transparent, 18px -18px 0 -3px transparent, 0 -26px 0 -3px transparent, -18px -18px 0 -3px transparent, -26px 0 0 -3px transparent, -18px 18px 0 -3px transparent, 0 26px 0 -3px transparent, 18px 18px 0 -3px transparent;
}
}
@keyframes tcb19-pop {
0% {
transform: scale(1);
}
30% {
transform: scale(0.8);
}
70% {
transform: scale(1.12);
}
100% {
transform: scale(1);
}
}
}
/* 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. */
.tcb-19 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tcb19-bg: #211a2e;
--color-tcb19-bg: oklch(0.2 0.03 300);
--color-tcb19-bg2: #16121f;
--color-tcb19-bg2: oklch(0.15 0.02 290);
--color-tcb19-btn: #2a2138;
--color-tcb19-btn: oklch(0.24 0.035 300);
--color-tcb19-p1: #f0a23a;
--color-tcb19-p1: oklch(0.77 0.16 65);
--color-tcb19-p2: #e04aa6;
--color-tcb19-p2: oklch(0.65 0.22 340);
--color-tcb19-x: #f7efe6;
--color-tcb19-x: oklch(0.95 0.02 60);
--color-tcb19-mut: #bcaecb;
--color-tcb19-mut: oklch(0.77 0.04 300);
--font-tcb19-sans: 'Geist', system-ui, sans-serif;
--font-tcb19-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tcb19-boom: tcb19-boom 0.55s cubic-bezier(0.2,0.7,0.3,1) forwards;
--animate-tcb19-pop: tcb19-pop 0.55s cubic-bezier(0.34,1.56,0.64,1) forwards;
@keyframes tcb19-boom {
0% {
background: var(--color-tcb19-p1);
box-shadow: 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2);
}
60% {
background: transparent;
box-shadow: 22px 0 0 0 var(--color-tcb19-p1), 15px -15px 0 0 var(--color-tcb19-p2), 0 -22px 0 0 var(--color-tcb19-p1), -15px -15px 0 0 var(--color-tcb19-p2), -22px 0 0 0 var(--color-tcb19-p1), -15px 15px 0 0 var(--color-tcb19-p2), 0 22px 0 0 var(--color-tcb19-p1), 15px 15px 0 0 var(--color-tcb19-p2);
}
100% {
background: transparent;
box-shadow: 26px 0 0 -3px transparent, 18px -18px 0 -3px transparent, 0 -26px 0 -3px transparent, -18px -18px 0 -3px transparent, -26px 0 0 -3px transparent, -18px 18px 0 -3px transparent, 0 26px 0 -3px transparent, 18px 18px 0 -3px transparent;
}
}
@keyframes tcb19-pop {
0% {
transform: scale(1);
}
30% {
transform: scale(0.8);
}
70% {
transform: scale(1.12);
}
100% {
transform: scale(1);
}
}
}
/* 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. */
.tcb-19 {
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=Geist:wght@400..800&family=Geist+Mono:wght@400..600&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=Geist:wght@400..800&family=Geist+Mono:wght@400..600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Both keyframes moved into
@theme; the eight-shadow burst now referencesvar(--color-tcb19-p1/p2)theme colours directly. - Bug fix: the CSS original ran on
:active, so quick taps killed the burst after a frame or two and keyboard Enter never showed it. Adata-burstattribute cleared onanimationendgives every input the full animation. - UI upgrade from the CSS original: reduced motion uses
animate-none!so it beats the data variant, and the stage caption rose to 7:1.
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: Particle Burst Tailwind Close Button
Source: https://codefronts.com/components/tailwind-css-close-buttons/particle-burst/
Eight particles from one 6px dot: a single keyframe animates eight box-shadow copies outward and shrinks them, while the × squashes and pops. The CSS original played it on :active, so a quick tap cut the burst off after a frame or two; the port triggers it with a data-burst attribute that the script clears on animationend, so every tap gets the full 550ms.
## HTML
```html
<section class="tcb-19 w-full min-h-svh grid place-items-center bg-[linear-gradient(180deg,var(--color-tcb19-bg),var(--color-tcb19-bg2))] px-6 py-12 font-tcb19-sans" aria-label="Particle burst close button demo">
<div class="grid w-full justify-items-center gap-7">
<button id="tcb-19-btn" type="button" aria-label="Close" class="group relative grid size-16 cursor-pointer place-items-center rounded-full border-[1.5px] border-tcb19-p1/40 bg-tcb19-btn shadow-[0_16px_36px_-18px_rgb(0_0_0/0.8)] transition-colors duration-200 hover:border-tcb19-p1 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-tcb19-p1 motion-reduce:transition-none">
<span aria-hidden="true" class="relative size-5 before:absolute before:top-1/2 before:left-1/2 before:h-[2.5px] before:w-5 before:-translate-1/2 before:rotate-45 before:rounded-full before:bg-tcb19-x after:absolute after:top-1/2 after:left-1/2 after:h-[2.5px] after:w-5 after:-translate-1/2 after:-rotate-45 after:rounded-full after:bg-tcb19-x group-data-burst:animate-tcb19-pop motion-reduce:animate-none!"></span>
<span data-burst-dot aria-hidden="true" class="pointer-events-none absolute top-1/2 left-1/2 -m-[3px] size-1.5 rounded-full group-data-burst:animate-tcb19-boom motion-reduce:animate-none!"></span>
</button>
<p aria-hidden="true" class="rounded-full px-3.5 py-2 font-tcb19-mono text-[11.5px] text-tcb19-mut ring-1 ring-white/12">8× box-shadow particles · group-data-burst: · animationend</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tcb19-bg: #211a2e;
--color-tcb19-bg: oklch(0.2 0.03 300);
--color-tcb19-bg2: #16121f;
--color-tcb19-bg2: oklch(0.15 0.02 290);
--color-tcb19-btn: #2a2138;
--color-tcb19-btn: oklch(0.24 0.035 300);
--color-tcb19-p1: #f0a23a;
--color-tcb19-p1: oklch(0.77 0.16 65);
--color-tcb19-p2: #e04aa6;
--color-tcb19-p2: oklch(0.65 0.22 340);
--color-tcb19-x: #f7efe6;
--color-tcb19-x: oklch(0.95 0.02 60);
--color-tcb19-mut: #bcaecb;
--color-tcb19-mut: oklch(0.77 0.04 300);
--font-tcb19-sans: 'Geist', system-ui, sans-serif;
--font-tcb19-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tcb19-boom: tcb19-boom 0.55s cubic-bezier(0.2,0.7,0.3,1) forwards;
--animate-tcb19-pop: tcb19-pop 0.55s cubic-bezier(0.34,1.56,0.64,1) forwards;
@keyframes tcb19-boom {
0% {
background: var(--color-tcb19-p1);
box-shadow: 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2);
}
60% {
background: transparent;
box-shadow: 22px 0 0 0 var(--color-tcb19-p1), 15px -15px 0 0 var(--color-tcb19-p2), 0 -22px 0 0 var(--color-tcb19-p1), -15px -15px 0 0 var(--color-tcb19-p2), -22px 0 0 0 var(--color-tcb19-p1), -15px 15px 0 0 var(--color-tcb19-p2), 0 22px 0 0 var(--color-tcb19-p1), 15px 15px 0 0 var(--color-tcb19-p2);
}
100% {
background: transparent;
box-shadow: 26px 0 0 -3px transparent, 18px -18px 0 -3px transparent, 0 -26px 0 -3px transparent, -18px -18px 0 -3px transparent, -26px 0 0 -3px transparent, -18px 18px 0 -3px transparent, 0 26px 0 -3px transparent, 18px 18px 0 -3px transparent;
}
}
@keyframes tcb19-pop {
0% {
transform: scale(1);
}
30% {
transform: scale(0.8);
}
70% {
transform: scale(1.12);
}
100% {
transform: scale(1);
}
}
}
/* 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. */
.tcb-19 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const btn = document.getElementById('tcb-19-btn');
if (!btn || btn.dataset.wired) return;
btn.dataset.wired = '1';
const dot = btn.querySelector('[data-burst-dot]');
const fire = () => {
btn.removeAttribute('data-burst');
requestAnimationFrame(() => requestAnimationFrame(() => btn.setAttribute('data-burst', '')));
};
btn.addEventListener('pointerdown', fire);
btn.addEventListener('keydown', (e) => { if ((e.key === 'Enter' || e.key === ' ') && !e.repeat) fire(); });
dot.addEventListener('animationend', () => btn.removeAttribute('data-burst'));
})();
```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: Particle Burst Tailwind Close Button
Source: https://codefronts.com/components/tailwind-css-close-buttons/particle-burst/
Eight particles from one 6px dot: a single keyframe animates eight box-shadow copies outward and shrinks them, while the × squashes and pops. The CSS original played it on :active, so a quick tap cut the burst off after a frame or two; the port triggers it with a data-burst attribute that the script clears on animationend, so every tap gets the full 550ms.
## HTML
```html
<section class="tcb-19 w-full min-h-svh grid place-items-center bg-[linear-gradient(180deg,var(--color-tcb19-bg),var(--color-tcb19-bg2))] px-6 py-12 font-tcb19-sans" aria-label="Particle burst close button demo">
<div class="grid w-full justify-items-center gap-7">
<button id="tcb-19-btn" type="button" aria-label="Close" class="group relative grid size-16 cursor-pointer place-items-center rounded-full border-[1.5px] border-tcb19-p1/40 bg-tcb19-btn shadow-[0_16px_36px_-18px_rgb(0_0_0/0.8)] transition-colors duration-200 hover:border-tcb19-p1 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-tcb19-p1 motion-reduce:transition-none">
<span aria-hidden="true" class="relative size-5 before:absolute before:top-1/2 before:left-1/2 before:h-[2.5px] before:w-5 before:-translate-1/2 before:rotate-45 before:rounded-full before:bg-tcb19-x after:absolute after:top-1/2 after:left-1/2 after:h-[2.5px] after:w-5 after:-translate-1/2 after:-rotate-45 after:rounded-full after:bg-tcb19-x group-data-burst:animate-tcb19-pop motion-reduce:animate-none!"></span>
<span data-burst-dot aria-hidden="true" class="pointer-events-none absolute top-1/2 left-1/2 -m-[3px] size-1.5 rounded-full group-data-burst:animate-tcb19-boom motion-reduce:animate-none!"></span>
</button>
<p aria-hidden="true" class="rounded-full px-3.5 py-2 font-tcb19-mono text-[11.5px] text-tcb19-mut ring-1 ring-white/12">8× box-shadow particles · group-data-burst: · animationend</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tcb19-bg: #211a2e;
--color-tcb19-bg: oklch(0.2 0.03 300);
--color-tcb19-bg2: #16121f;
--color-tcb19-bg2: oklch(0.15 0.02 290);
--color-tcb19-btn: #2a2138;
--color-tcb19-btn: oklch(0.24 0.035 300);
--color-tcb19-p1: #f0a23a;
--color-tcb19-p1: oklch(0.77 0.16 65);
--color-tcb19-p2: #e04aa6;
--color-tcb19-p2: oklch(0.65 0.22 340);
--color-tcb19-x: #f7efe6;
--color-tcb19-x: oklch(0.95 0.02 60);
--color-tcb19-mut: #bcaecb;
--color-tcb19-mut: oklch(0.77 0.04 300);
--font-tcb19-sans: 'Geist', system-ui, sans-serif;
--font-tcb19-mono: 'Geist Mono', ui-monospace, monospace;
--animate-tcb19-boom: tcb19-boom 0.55s cubic-bezier(0.2,0.7,0.3,1) forwards;
--animate-tcb19-pop: tcb19-pop 0.55s cubic-bezier(0.34,1.56,0.64,1) forwards;
@keyframes tcb19-boom {
0% {
background: var(--color-tcb19-p1);
box-shadow: 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2), 0 0 0 0 var(--color-tcb19-p1), 0 0 0 0 var(--color-tcb19-p2);
}
60% {
background: transparent;
box-shadow: 22px 0 0 0 var(--color-tcb19-p1), 15px -15px 0 0 var(--color-tcb19-p2), 0 -22px 0 0 var(--color-tcb19-p1), -15px -15px 0 0 var(--color-tcb19-p2), -22px 0 0 0 var(--color-tcb19-p1), -15px 15px 0 0 var(--color-tcb19-p2), 0 22px 0 0 var(--color-tcb19-p1), 15px 15px 0 0 var(--color-tcb19-p2);
}
100% {
background: transparent;
box-shadow: 26px 0 0 -3px transparent, 18px -18px 0 -3px transparent, 0 -26px 0 -3px transparent, -18px -18px 0 -3px transparent, -26px 0 0 -3px transparent, -18px 18px 0 -3px transparent, 0 26px 0 -3px transparent, 18px 18px 0 -3px transparent;
}
}
@keyframes tcb19-pop {
0% {
transform: scale(1);
}
30% {
transform: scale(0.8);
}
70% {
transform: scale(1.12);
}
100% {
transform: scale(1);
}
}
}
/* 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. */
.tcb-19 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const btn = document.getElementById('tcb-19-btn');
if (!btn || btn.dataset.wired) return;
btn.dataset.wired = '1';
const dot = btn.querySelector('[data-burst-dot]');
const fire = () => {
btn.removeAttribute('data-burst');
requestAnimationFrame(() => requestAnimationFrame(() => btn.setAttribute('data-burst', '')));
};
btn.addEventListener('pointerdown', fire);
btn.addEventListener('keydown', (e) => { if ((e.key === 'Enter' || e.key === ' ') && !e.repeat) fire(); });
dot.addEventListener('animationend', () => btn.removeAttribute('data-burst'));
})();
```How this works
Why not :active. :active ends the instant the pointer lifts; on a fast tap that is ~60ms, and the animation is torn down mid-flight. An attribute set on pointerdown and cleared on animationend decouples the effect from the gesture:
btn.setAttribute('data-burst', '');
burst.addEventListener('animationend', () => btn.removeAttribute('data-burst'));The particles are the eight compass points at 22px, then 26px with a -3px spread that shrinks each dot to nothing.
Make it yours
- More particles: add shadow layers at the 22.5° points (keep the same count in every keyframe).
- Two-colour burst: already alternating
p1/p2; add a third token for confetti. - Bigger blast: scale the offsets (22/15 → 30/21).
Gotchas — read before shipping
- Every keyframe must list the same number of shadows or the browser cannot interpolate them.
- Re-triggering while the attribute is still set does nothing — remove it first (the script handles this).
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
The floor is Tailwind v4's generated sheet.