20 / 51
All 51 in 51 Tailwind CSS Buttons
- 01Tailwind Submit Button (Form Primary)
- 02Tailwind Primary and Secondary Button Pair
- 03Tailwind Ghost / Outline Button
- 04Tailwind Icon-Only Button with Accessible Tooltip
- 05Tailwind Gradient CTA Button
- 06Tailwind Loading Spinner Button (State Transition)
- 07Tailwind Disabled Button State (Full State Reference)
- 08Tailwind Button Group Component (Segmented Radio-Like)
- 09Tailwind Add to Cart Button
- 10Tailwind Download Progress Button
- 11Tailwind Like Heart Button
- 12Tailwind Confirm Delete Button
- 13Tailwind Play Pause Button
- 14Tailwind Subscribe Confetti Button
- 15Tailwind Magnetic Mercury Ripple Button
- 16Tailwind Brutalist Glitch Button
- 17Tailwind Neon Plasma Burst Button
- 18Tailwind Clay Press Button
- 19Tailwind Editorial Ink Spread Button
- 20Tailwind Particle Shard Burst Button
- 21Tailwind Brushed Brass Plate Button
- 22Tailwind Washi Paper Crane Button
- 23Tailwind Frosted Ice Cube Button
- 24Tailwind Vintage Typewriter Key Button
- 25Tailwind Lacquered Walnut Wood Button
- 26Tailwind Pharma Gel Capsule Button
- 27Tailwind Aged Copper Coin Button
- 28Tailwind Origami Fortune Cookie Button
- 29Tailwind Holographic Foil Sticker Button
- 30Tailwind Brutalist Concrete Slab Button
- 31Tailwind Mercury Bead Morph Button
- 32Tailwind Soft Pill Modern SaaS Button
- 33Tailwind Masking Tape Label Button
- 34Tailwind Obsidian Glass Shard Button
- 35Tailwind Wine Velvet Ribbon Button
- 36Tailwind LED Segment Display Button
- 37Tailwind Cassette Mixtape Label Button
- 38Tailwind Chalkboard Menu Button
- 39Tailwind Vinyl Record Sleeve Ripple Button
- 40Tailwind Polaroid Snapshot Button
- 41Tailwind Airline Boarding Pass Button
- 42Tailwind Aged Brass Subway Token Button
- 43Tailwind Vintage Rx Pharmacy Label Button
- 44Tailwind Bakelite Radio Dial Toggle Button
- 45Tailwind Regal Postage Stamp Button
- 46Tailwind Victorian Cigar Band Foil Button
- 47Tailwind Vintage Calculator Key Button
- 48Tailwind Toolbox Latch Spotlight Button
- 49Tailwind Receipt Paper Total Count-Up Button
- 50Tailwind IBM Punch Card Magnetic Follow Button
- 51Tailwind Brass Periscope Lens Button
Tailwind Particle Shard Burst Button
Every press shatters a ring of glass shards outward, each with its own angle, distance, spin and lifetime — spawned as real elements, animated by one keyframe, and removed on animationend so the DOM never grows. The shard's look is a class list the JS applies; its trajectory is inline data.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tb-20 w-full min-h-svh grid place-items-center font-tb20-sans p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="tb-20__btn relative cursor-pointer grid place-items-center min-h-14 px-8 border border-tb20-line rounded-[0.85rem] text-[0.9375rem] font-semibold tracking-[0.02em] text-tb20-ink bg-[linear-gradient(180deg,var(--color-tb20-btn-lift),var(--color-tb20-btn))] shadow-[0_16px_34px_-20px_#000000e6] transition-[scale,border-color] duration-250 ease-tb20-out [-webkit-tap-highlight-color:transparent] hover:border-tb20-line-hot active:scale-95 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tb20-s1 motion-reduce:transition-none">
<span aria-hidden="true" class="tb-20__shards absolute inset-0 z-10 pointer-events-none"></span>
<span class="relative z-20">Shatter</span>
</button>
</div><div class="tb-20 w-full min-h-svh grid place-items-center font-tb20-sans p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="tb-20__btn relative cursor-pointer grid place-items-center min-h-14 px-8 border border-tb20-line rounded-[0.85rem] text-[0.9375rem] font-semibold tracking-[0.02em] text-tb20-ink bg-[linear-gradient(180deg,var(--color-tb20-btn-lift),var(--color-tb20-btn))] shadow-[0_16px_34px_-20px_#000000e6] transition-[scale,border-color] duration-250 ease-tb20-out [-webkit-tap-highlight-color:transparent] hover:border-tb20-line-hot active:scale-95 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tb20-s1 motion-reduce:transition-none">
<span aria-hidden="true" class="tb-20__shards absolute inset-0 z-10 pointer-events-none"></span>
<span class="relative z-20">Shatter</span>
</button>
</div>@import "tailwindcss";
@theme {
--color-tb20-bg: #0a0a10;
--color-tb20-bg: oklch(0.12 0.015 285);
--color-tb20-btn: #15151f;
--color-tb20-btn: oklch(0.22 0.02 285);
--color-tb20-btn-lift: #1d1d29;
--color-tb20-btn-lift: oklch(0.26 0.02 285);
--color-tb20-line: #2b2b3a;
--color-tb20-line: oklch(0.33 0.02 285);
--color-tb20-line-hot: #4d7d94;
--color-tb20-line-hot: oklch(0.58 0.07 226);
--color-tb20-ink: #f4f4fa;
--color-tb20-ink: oklch(0.97 0.004 285);
--color-tb20-s1: #7dd3fc;
--color-tb20-s1: oklch(0.84 0.11 226);
--color-tb20-s2: #c4b5fd;
--color-tb20-s2: oklch(0.82 0.1 293);
--color-tb20-s3: #fda4af;
--color-tb20-s3: oklch(0.82 0.1 15);
--font-tb20-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb20-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb20-fly: tb-20-fly var(--t, 700ms) cubic-bezier(0.14, 0.8, 0.3, 1) forwards;
}
@keyframes tb-20-fly {
0% {
translate: 0 0;
scale: 0.35;
opacity: 1;
}
100% {
translate: 0 calc(var(--d, 60px) * -1);
scale: 0.95;
opacity: 0;
}
}
.tb-20 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(40rem 24rem at 50% 50%, color-mix(in oklab, var(--color-tb20-s2) 10%, transparent), transparent 72%), var(--color-tb20-bg);
}
/* Each shard's own angle: set inline by the controller, applied here because a
rotate utility cannot read a per-element variable. */
.tb-20__shards > span {
rotate: var(--a, 0deg);
}
@media (prefers-color-scheme: light) {
.tb-20:not([data-theme="dark"]) {
--color-tb20-bg: oklch(0.95 0.008 285);
}
}
.tb-20[data-theme="light"] {
--color-tb20-bg: oklch(0.95 0.008 285);
}
@media (forced-colors: active) {
.tb-20__btn {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-20__shards {
display: none;
}
}@import "tailwindcss";
@theme {
--color-tb20-bg: #0a0a10;
--color-tb20-bg: oklch(0.12 0.015 285);
--color-tb20-btn: #15151f;
--color-tb20-btn: oklch(0.22 0.02 285);
--color-tb20-btn-lift: #1d1d29;
--color-tb20-btn-lift: oklch(0.26 0.02 285);
--color-tb20-line: #2b2b3a;
--color-tb20-line: oklch(0.33 0.02 285);
--color-tb20-line-hot: #4d7d94;
--color-tb20-line-hot: oklch(0.58 0.07 226);
--color-tb20-ink: #f4f4fa;
--color-tb20-ink: oklch(0.97 0.004 285);
--color-tb20-s1: #7dd3fc;
--color-tb20-s1: oklch(0.84 0.11 226);
--color-tb20-s2: #c4b5fd;
--color-tb20-s2: oklch(0.82 0.1 293);
--color-tb20-s3: #fda4af;
--color-tb20-s3: oklch(0.82 0.1 15);
--font-tb20-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb20-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb20-fly: tb-20-fly var(--t, 700ms) cubic-bezier(0.14, 0.8, 0.3, 1) forwards;
}
@keyframes tb-20-fly {
0% {
translate: 0 0;
scale: 0.35;
opacity: 1;
}
100% {
translate: 0 calc(var(--d, 60px) * -1);
scale: 0.95;
opacity: 0;
}
}
.tb-20 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(40rem 24rem at 50% 50%, color-mix(in oklab, var(--color-tb20-s2) 10%, transparent), transparent 72%), var(--color-tb20-bg);
}
/* Each shard's own angle: set inline by the controller, applied here because a
rotate utility cannot read a per-element variable. */
.tb-20__shards > span {
rotate: var(--a, 0deg);
}
@media (prefers-color-scheme: light) {
.tb-20:not([data-theme="dark"]) {
--color-tb20-bg: oklch(0.95 0.008 285);
}
}
.tb-20[data-theme="light"] {
--color-tb20-bg: oklch(0.95 0.008 285);
}
@media (forced-colors: active) {
.tb-20__btn {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-20__shards {
display: none;
}
}(() => {
const btn = document.querySelector('.tb-20__btn');
const layer = btn && btn.querySelector('.tb-20__shards');
if (!layer) return;
const calm = matchMedia('(prefers-reduced-motion: reduce)');
// The particle's SKIN lives here as Tailwind classes; its TRAJECTORY is inline data.
const BASE = 'absolute top-1/2 left-1/2 opacity-0 animate-tb20-fly ';
const SLIVER = BASE + 'w-3 h-[0.28rem] -mt-[0.14rem] -ml-1.5 [clip-path:polygon(0_50%,55%_0,100%_42%,62%_100%)] ';
const CHIP = BASE + 'size-2 -mt-1 -ml-1 [clip-path:polygon(50%_0,100%_38%,80%_100%,18%_92%,0_34%)] ';
btn.addEventListener('click', () => {
if (calm.matches || layer.childElementCount > 40) return;
for (let i = 0; i < 12; i++) {
const s = document.createElement('span');
s.className = i % 3 === 0 ? CHIP + 'bg-tb20-s2' : (i % 3 === 1 ? SLIVER + 'bg-tb20-s3' : SLIVER + 'bg-tb20-s1');
s.style.setProperty('--a', ((i / 12) * 360 + (Math.random() * 22 - 11)).toFixed(1) + 'deg');
s.style.setProperty('--d', (46 + Math.random() * 54).toFixed(0) + 'px');
s.style.setProperty('--t', (520 + Math.random() * 380).toFixed(0) + 'ms');
s.addEventListener('animationend', () => s.remove());
layer.appendChild(s);
}
});
})();(() => {
const btn = document.querySelector('.tb-20__btn');
const layer = btn && btn.querySelector('.tb-20__shards');
if (!layer) return;
const calm = matchMedia('(prefers-reduced-motion: reduce)');
// The particle's SKIN lives here as Tailwind classes; its TRAJECTORY is inline data.
const BASE = 'absolute top-1/2 left-1/2 opacity-0 animate-tb20-fly ';
const SLIVER = BASE + 'w-3 h-[0.28rem] -mt-[0.14rem] -ml-1.5 [clip-path:polygon(0_50%,55%_0,100%_42%,62%_100%)] ';
const CHIP = BASE + 'size-2 -mt-1 -ml-1 [clip-path:polygon(50%_0,100%_38%,80%_100%,18%_92%,0_34%)] ';
btn.addEventListener('click', () => {
if (calm.matches || layer.childElementCount > 40) return;
for (let i = 0; i < 12; i++) {
const s = document.createElement('span');
s.className = i % 3 === 0 ? CHIP + 'bg-tb20-s2' : (i % 3 === 1 ? SLIVER + 'bg-tb20-s3' : SLIVER + 'bg-tb20-s1');
s.style.setProperty('--a', ((i / 12) * 360 + (Math.random() * 22 - 11)).toFixed(1) + 'deg');
s.style.setProperty('--d', (46 + Math.random() * 54).toFixed(0) + 'px');
s.style.setProperty('--t', (520 + Math.random() * 380).toFixed(0) + 'ms');
s.addEventListener('animationend', () => s.remove());
layer.appendChild(s);
}
});
})();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-tb20-bg: #0a0a10;
--color-tb20-bg: oklch(0.12 0.015 285);
--color-tb20-btn: #15151f;
--color-tb20-btn: oklch(0.22 0.02 285);
--color-tb20-btn-lift: #1d1d29;
--color-tb20-btn-lift: oklch(0.26 0.02 285);
--color-tb20-line: #2b2b3a;
--color-tb20-line: oklch(0.33 0.02 285);
--color-tb20-line-hot: #4d7d94;
--color-tb20-line-hot: oklch(0.58 0.07 226);
--color-tb20-ink: #f4f4fa;
--color-tb20-ink: oklch(0.97 0.004 285);
--color-tb20-s1: #7dd3fc;
--color-tb20-s1: oklch(0.84 0.11 226);
--color-tb20-s2: #c4b5fd;
--color-tb20-s2: oklch(0.82 0.1 293);
--color-tb20-s3: #fda4af;
--color-tb20-s3: oklch(0.82 0.1 15);
--font-tb20-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb20-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb20-fly: tb-20-fly var(--t, 700ms) cubic-bezier(0.14, 0.8, 0.3, 1) forwards;
}
@keyframes tb-20-fly {
0% {
translate: 0 0;
scale: 0.35;
opacity: 1;
}
100% {
translate: 0 calc(var(--d, 60px) * -1);
scale: 0.95;
opacity: 0;
}
}
.tb-20 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(40rem 24rem at 50% 50%, color-mix(in oklab, var(--color-tb20-s2) 10%, transparent), transparent 72%), var(--color-tb20-bg);
}
/* Each shard's own angle: set inline by the controller, applied here because a
rotate utility cannot read a per-element variable. */
.tb-20__shards > span {
rotate: var(--a, 0deg);
}
@media (prefers-color-scheme: light) {
.tb-20:not([data-theme="dark"]) {
--color-tb20-bg: oklch(0.95 0.008 285);
}
}
.tb-20[data-theme="light"] {
--color-tb20-bg: oklch(0.95 0.008 285);
}
@media (forced-colors: active) {
.tb-20__btn {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-20__shards {
display: none;
}
}@import "tailwindcss";
@theme {
--color-tb20-bg: #0a0a10;
--color-tb20-bg: oklch(0.12 0.015 285);
--color-tb20-btn: #15151f;
--color-tb20-btn: oklch(0.22 0.02 285);
--color-tb20-btn-lift: #1d1d29;
--color-tb20-btn-lift: oklch(0.26 0.02 285);
--color-tb20-line: #2b2b3a;
--color-tb20-line: oklch(0.33 0.02 285);
--color-tb20-line-hot: #4d7d94;
--color-tb20-line-hot: oklch(0.58 0.07 226);
--color-tb20-ink: #f4f4fa;
--color-tb20-ink: oklch(0.97 0.004 285);
--color-tb20-s1: #7dd3fc;
--color-tb20-s1: oklch(0.84 0.11 226);
--color-tb20-s2: #c4b5fd;
--color-tb20-s2: oklch(0.82 0.1 293);
--color-tb20-s3: #fda4af;
--color-tb20-s3: oklch(0.82 0.1 15);
--font-tb20-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb20-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb20-fly: tb-20-fly var(--t, 700ms) cubic-bezier(0.14, 0.8, 0.3, 1) forwards;
}
@keyframes tb-20-fly {
0% {
translate: 0 0;
scale: 0.35;
opacity: 1;
}
100% {
translate: 0 calc(var(--d, 60px) * -1);
scale: 0.95;
opacity: 0;
}
}
.tb-20 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(40rem 24rem at 50% 50%, color-mix(in oklab, var(--color-tb20-s2) 10%, transparent), transparent 72%), var(--color-tb20-bg);
}
/* Each shard's own angle: set inline by the controller, applied here because a
rotate utility cannot read a per-element variable. */
.tb-20__shards > span {
rotate: var(--a, 0deg);
}
@media (prefers-color-scheme: light) {
.tb-20:not([data-theme="dark"]) {
--color-tb20-bg: oklch(0.95 0.008 285);
}
}
.tb-20[data-theme="light"] {
--color-tb20-bg: oklch(0.95 0.008 285);
}
@media (forced-colors: active) {
.tb-20__btn {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-20__shards {
display: none;
}
}<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@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@500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Utilities: the button, and each shard's entire appearance via class-list constants in the controller. CSS: the keyframe, the stage backdrop, and one rule applying each shard's inline
--atorotate— a rotate utility cannot read a per-element variable, androtate-[var(--a)]would compile a single static rule that every shard would share. - Generated elements are the port's genuinely awkward case. There is no markup to hold utilities, so the class list becomes a named constant in JS. That is better than it sounds: the skin stays in Tailwind, devtools shows real utility classes on every shard, and the three shard variants are named (
SLIVER,CHIP) instead of hiding innth-child(3n). - A CLI build cannot see classes that only exist in a JS string — safelist them, or keep the constants in a file the content globs cover. The browser build used here picks them up from the live DOM.
- The animation token carries
var(--t, 700ms)and the keyframevar(--d, 60px), so a shard with no inline data still flies. Defaults in tokens are cheap insurance for JS-driven particles.
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 Particle Shard Burst Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-particle-shard-burst-button/
Every press shatters a ring of glass shards outward, each with its own angle, distance, spin and lifetime — spawned as real elements, animated by one keyframe, and removed on animationend so the DOM never grows. The shard's look is a class list the JS applies; its trajectory is inline data.
## HTML
```html
<div class="tb-20 w-full min-h-svh grid place-items-center font-tb20-sans p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="tb-20__btn relative cursor-pointer grid place-items-center min-h-14 px-8 border border-tb20-line rounded-[0.85rem] text-[0.9375rem] font-semibold tracking-[0.02em] text-tb20-ink bg-[linear-gradient(180deg,var(--color-tb20-btn-lift),var(--color-tb20-btn))] shadow-[0_16px_34px_-20px_#000000e6] transition-[scale,border-color] duration-250 ease-tb20-out [-webkit-tap-highlight-color:transparent] hover:border-tb20-line-hot active:scale-95 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tb20-s1 motion-reduce:transition-none">
<span aria-hidden="true" class="tb-20__shards absolute inset-0 z-10 pointer-events-none"></span>
<span class="relative z-20">Shatter</span>
</button>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb20-bg: #0a0a10;
--color-tb20-bg: oklch(0.12 0.015 285);
--color-tb20-btn: #15151f;
--color-tb20-btn: oklch(0.22 0.02 285);
--color-tb20-btn-lift: #1d1d29;
--color-tb20-btn-lift: oklch(0.26 0.02 285);
--color-tb20-line: #2b2b3a;
--color-tb20-line: oklch(0.33 0.02 285);
--color-tb20-line-hot: #4d7d94;
--color-tb20-line-hot: oklch(0.58 0.07 226);
--color-tb20-ink: #f4f4fa;
--color-tb20-ink: oklch(0.97 0.004 285);
--color-tb20-s1: #7dd3fc;
--color-tb20-s1: oklch(0.84 0.11 226);
--color-tb20-s2: #c4b5fd;
--color-tb20-s2: oklch(0.82 0.1 293);
--color-tb20-s3: #fda4af;
--color-tb20-s3: oklch(0.82 0.1 15);
--font-tb20-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb20-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb20-fly: tb-20-fly var(--t, 700ms) cubic-bezier(0.14, 0.8, 0.3, 1) forwards;
}
@keyframes tb-20-fly {
0% {
translate: 0 0;
scale: 0.35;
opacity: 1;
}
100% {
translate: 0 calc(var(--d, 60px) * -1);
scale: 0.95;
opacity: 0;
}
}
.tb-20 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(40rem 24rem at 50% 50%, color-mix(in oklab, var(--color-tb20-s2) 10%, transparent), transparent 72%), var(--color-tb20-bg);
}
/* Each shard's own angle: set inline by the controller, applied here because a
rotate utility cannot read a per-element variable. */
.tb-20__shards > span {
rotate: var(--a, 0deg);
}
@media (prefers-color-scheme: light) {
.tb-20:not([data-theme="dark"]) {
--color-tb20-bg: oklch(0.95 0.008 285);
}
}
.tb-20[data-theme="light"] {
--color-tb20-bg: oklch(0.95 0.008 285);
}
@media (forced-colors: active) {
.tb-20__btn {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-20__shards {
display: none;
}
}
```
## JavaScript
```js
(() => {
const btn = document.querySelector('.tb-20__btn');
const layer = btn && btn.querySelector('.tb-20__shards');
if (!layer) return;
const calm = matchMedia('(prefers-reduced-motion: reduce)');
// The particle's SKIN lives here as Tailwind classes; its TRAJECTORY is inline data.
const BASE = 'absolute top-1/2 left-1/2 opacity-0 animate-tb20-fly ';
const SLIVER = BASE + 'w-3 h-[0.28rem] -mt-[0.14rem] -ml-1.5 [clip-path:polygon(0_50%,55%_0,100%_42%,62%_100%)] ';
const CHIP = BASE + 'size-2 -mt-1 -ml-1 [clip-path:polygon(50%_0,100%_38%,80%_100%,18%_92%,0_34%)] ';
btn.addEventListener('click', () => {
if (calm.matches || layer.childElementCount > 40) return;
for (let i = 0; i < 12; i++) {
const s = document.createElement('span');
s.className = i % 3 === 0 ? CHIP + 'bg-tb20-s2' : (i % 3 === 1 ? SLIVER + 'bg-tb20-s3' : SLIVER + 'bg-tb20-s1');
s.style.setProperty('--a', ((i / 12) * 360 + (Math.random() * 22 - 11)).toFixed(1) + 'deg');
s.style.setProperty('--d', (46 + Math.random() * 54).toFixed(0) + 'px');
s.style.setProperty('--t', (520 + Math.random() * 380).toFixed(0) + 'ms');
s.addEventListener('animationend', () => s.remove());
layer.appendChild(s);
}
});
})();
```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 Particle Shard Burst Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-particle-shard-burst-button/
Every press shatters a ring of glass shards outward, each with its own angle, distance, spin and lifetime — spawned as real elements, animated by one keyframe, and removed on animationend so the DOM never grows. The shard's look is a class list the JS applies; its trajectory is inline data.
## HTML
```html
<div class="tb-20 w-full min-h-svh grid place-items-center font-tb20-sans p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" class="tb-20__btn relative cursor-pointer grid place-items-center min-h-14 px-8 border border-tb20-line rounded-[0.85rem] text-[0.9375rem] font-semibold tracking-[0.02em] text-tb20-ink bg-[linear-gradient(180deg,var(--color-tb20-btn-lift),var(--color-tb20-btn))] shadow-[0_16px_34px_-20px_#000000e6] transition-[scale,border-color] duration-250 ease-tb20-out [-webkit-tap-highlight-color:transparent] hover:border-tb20-line-hot active:scale-95 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tb20-s1 motion-reduce:transition-none">
<span aria-hidden="true" class="tb-20__shards absolute inset-0 z-10 pointer-events-none"></span>
<span class="relative z-20">Shatter</span>
</button>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb20-bg: #0a0a10;
--color-tb20-bg: oklch(0.12 0.015 285);
--color-tb20-btn: #15151f;
--color-tb20-btn: oklch(0.22 0.02 285);
--color-tb20-btn-lift: #1d1d29;
--color-tb20-btn-lift: oklch(0.26 0.02 285);
--color-tb20-line: #2b2b3a;
--color-tb20-line: oklch(0.33 0.02 285);
--color-tb20-line-hot: #4d7d94;
--color-tb20-line-hot: oklch(0.58 0.07 226);
--color-tb20-ink: #f4f4fa;
--color-tb20-ink: oklch(0.97 0.004 285);
--color-tb20-s1: #7dd3fc;
--color-tb20-s1: oklch(0.84 0.11 226);
--color-tb20-s2: #c4b5fd;
--color-tb20-s2: oklch(0.82 0.1 293);
--color-tb20-s3: #fda4af;
--color-tb20-s3: oklch(0.82 0.1 15);
--font-tb20-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb20-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb20-fly: tb-20-fly var(--t, 700ms) cubic-bezier(0.14, 0.8, 0.3, 1) forwards;
}
@keyframes tb-20-fly {
0% {
translate: 0 0;
scale: 0.35;
opacity: 1;
}
100% {
translate: 0 calc(var(--d, 60px) * -1);
scale: 0.95;
opacity: 0;
}
}
.tb-20 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(40rem 24rem at 50% 50%, color-mix(in oklab, var(--color-tb20-s2) 10%, transparent), transparent 72%), var(--color-tb20-bg);
}
/* Each shard's own angle: set inline by the controller, applied here because a
rotate utility cannot read a per-element variable. */
.tb-20__shards > span {
rotate: var(--a, 0deg);
}
@media (prefers-color-scheme: light) {
.tb-20:not([data-theme="dark"]) {
--color-tb20-bg: oklch(0.95 0.008 285);
}
}
.tb-20[data-theme="light"] {
--color-tb20-bg: oklch(0.95 0.008 285);
}
@media (forced-colors: active) {
.tb-20__btn {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-20__shards {
display: none;
}
}
```
## JavaScript
```js
(() => {
const btn = document.querySelector('.tb-20__btn');
const layer = btn && btn.querySelector('.tb-20__shards');
if (!layer) return;
const calm = matchMedia('(prefers-reduced-motion: reduce)');
// The particle's SKIN lives here as Tailwind classes; its TRAJECTORY is inline data.
const BASE = 'absolute top-1/2 left-1/2 opacity-0 animate-tb20-fly ';
const SLIVER = BASE + 'w-3 h-[0.28rem] -mt-[0.14rem] -ml-1.5 [clip-path:polygon(0_50%,55%_0,100%_42%,62%_100%)] ';
const CHIP = BASE + 'size-2 -mt-1 -ml-1 [clip-path:polygon(50%_0,100%_38%,80%_100%,18%_92%,0_34%)] ';
btn.addEventListener('click', () => {
if (calm.matches || layer.childElementCount > 40) return;
for (let i = 0; i < 12; i++) {
const s = document.createElement('span');
s.className = i % 3 === 0 ? CHIP + 'bg-tb20-s2' : (i % 3 === 1 ? SLIVER + 'bg-tb20-s3' : SLIVER + 'bg-tb20-s1');
s.style.setProperty('--a', ((i / 12) * 360 + (Math.random() * 22 - 11)).toFixed(1) + 'deg');
s.style.setProperty('--d', (46 + Math.random() * 54).toFixed(0) + 'px');
s.style.setProperty('--t', (520 + Math.random() * 380).toFixed(0) + 'ms');
s.addEventListener('animationend', () => s.remove());
layer.appendChild(s);
}
});
})();
```How this works
The port moves the shard's skin into JS as a class string:
const SHARD = 'absolute top-1/2 left-1/2 w-3 h-[0.28rem] -mt-[0.14rem] -ml-1.5 ' +
'bg-tb20-s1 [clip-path:polygon(0_50%,55%_0,100%_42%,62%_100%)] ' +
'opacity-0 animate-tb20-fly';
s.className = SHARD + (i % 3 === 0 ? ' bg-tb20-s2 size-2 [clip-path:polygon(...)]' : '');That is the honest answer for JS-generated elements: they have no markup to carry utilities, so the class list becomes a named constant at the top of the controller — still Tailwind, still inspectable in devtools, and the variant per shard is explicit instead of hidden in an nth-child rule.
No per-frame JavaScript runs. Each shard is rotated to its angle on the element and translated along its own local Y inside the keyframe, so one keyframe produces a full radial spray on the compositor.
Cleanup is mandatory. animationend removes each node; a concurrency cap stops a spammed button from queueing a hundred animations.
Make it yours
- Count, spread and lifetime are all in the loop: 12 shards is a button, 30 a celebration, 60 a performance problem.
- Change the
clip-pathin the class constant for confetti (rectangles), sparks (slivers) or petals (rounded). - Spawn at the pointer by writing
--ox/--oyfrom the click coordinates onto the layer. - Floatier feel:
cubic-bezier(0.1,0.9,0.2,1)in the animation token. - Skin-independent — pair the burst layer with any other button in the collection.
Gotchas — read before shipping
- Always remove particles on
animationend. Fifty presses without cleanup leaves 600 dead nodes. - Un-registered custom properties cannot be interpolated, but they can be read inside an animated typed property — which is why
--dworks insidetranslate'scalc(). - Spawn on
click, notpointerdown, or keyboard activation produces no burst. - Cap concurrent bursts. A hundred simultaneous animations drops frames on low-end Android.
- Under reduced motion, return early in JS rather than hiding shards in CSS — you save the DOM work too.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Individual transform properties, clip-path polygons and animationend are all baseline below Tailwind v4's floor. Note that JS-applied utility classes must exist in the compiled sheet — with the browser build they are picked up from the DOM automatically; with a CLI build, safelist them.