09 / 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 Add to Cart Button
The most-measured button in e-commerce: press it and a cart glyph arcs into a badge, the digit bumps, and the label swaps to "Added" before returning to idle — with the real count announced politely, so screen-reader users hear the outcome instead of watching it.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tb-09 w-full min-h-svh grid place-items-center bg-tb09-bg font-tb09-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" data-state="idle" style="--tb-09-dx:6.5rem;--tb-09-dy:3.2rem" class="tb-09__btn group relative cursor-pointer grid items-center min-h-14 px-7 border-0 rounded-[0.85rem] text-base font-bold tracking-[-0.01em] text-tb09-ink bg-tb09-accent shadow-[0_12px_26px_-14px_var(--color-tb09-glow)] transition-[translate,background-color,scale] duration-200 ease-tb09-out data-[state=done]:bg-tb09-ok hover:-translate-y-0.5 active:translate-y-px active:scale-[0.99] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tb09-accent motion-reduce:transition-none motion-reduce:hover:translate-y-0">
<span class="col-start-1 row-start-1 flex items-center justify-center gap-2.5 opacity-0 scale-95 transition-[opacity,scale] duration-200 ease-tb09-out group-data-[state=idle]:opacity-100 group-data-[state=idle]:scale-100 motion-reduce:transition-none">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-[1.2rem] shrink-0"><path d="M3 4h2.2l2.3 10.5h9.8L19.5 7H6"/><circle cx="9.5" cy="19" r="1.5"/><circle cx="16.5" cy="19" r="1.5"/></svg>
<span>Add to cart — $148</span>
</span>
<span class="col-start-1 row-start-1 flex items-center justify-center gap-2.5 opacity-0 scale-95 transition-[opacity,scale] duration-200 ease-tb09-out group-data-[state=done]:opacity-100 group-data-[state=done]:scale-100 motion-reduce:transition-none">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" class="size-[1.2rem] shrink-0"><path d="M5 12.5 10 17.5 19 6.5"/></svg>
<span>Added to cart</span>
</span>
<span aria-hidden="true" class="absolute left-7 top-1/2 -mt-[0.6rem] size-[1.2rem] opacity-0 pointer-events-none text-tb09-ink group-[.is-fly]:animate-tb09-fly motion-reduce:hidden">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-full"><path d="M3 4h2.2l2.3 10.5h9.8L19.5 7H6"/></svg>
</span>
<span aria-hidden="true" class="tb-09__badge absolute -top-2 -right-2 min-w-7 h-7 px-1.5 grid place-items-center rounded-full text-[0.8125rem] font-extrabold tabular-nums text-tb09-badge-ink bg-tb09-badge shadow-[0_2px_8px_-2px_var(--color-tb09-badge-glow)] [&.is-bump]:animate-tb09-bump motion-reduce:[&.is-bump]:animate-none motion-reduce:[&.is-bump]:outline-2 motion-reduce:[&.is-bump]:outline-tb09-badge motion-reduce:[&.is-bump]:outline-offset-2">2</span>
</button>
<span role="status" aria-live="polite" class="tb-09__live sr-only"></span>
</div><div class="tb-09 w-full min-h-svh grid place-items-center bg-tb09-bg font-tb09-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" data-state="idle" style="--tb-09-dx:6.5rem;--tb-09-dy:3.2rem" class="tb-09__btn group relative cursor-pointer grid items-center min-h-14 px-7 border-0 rounded-[0.85rem] text-base font-bold tracking-[-0.01em] text-tb09-ink bg-tb09-accent shadow-[0_12px_26px_-14px_var(--color-tb09-glow)] transition-[translate,background-color,scale] duration-200 ease-tb09-out data-[state=done]:bg-tb09-ok hover:-translate-y-0.5 active:translate-y-px active:scale-[0.99] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tb09-accent motion-reduce:transition-none motion-reduce:hover:translate-y-0">
<span class="col-start-1 row-start-1 flex items-center justify-center gap-2.5 opacity-0 scale-95 transition-[opacity,scale] duration-200 ease-tb09-out group-data-[state=idle]:opacity-100 group-data-[state=idle]:scale-100 motion-reduce:transition-none">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-[1.2rem] shrink-0"><path d="M3 4h2.2l2.3 10.5h9.8L19.5 7H6"/><circle cx="9.5" cy="19" r="1.5"/><circle cx="16.5" cy="19" r="1.5"/></svg>
<span>Add to cart — $148</span>
</span>
<span class="col-start-1 row-start-1 flex items-center justify-center gap-2.5 opacity-0 scale-95 transition-[opacity,scale] duration-200 ease-tb09-out group-data-[state=done]:opacity-100 group-data-[state=done]:scale-100 motion-reduce:transition-none">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" class="size-[1.2rem] shrink-0"><path d="M5 12.5 10 17.5 19 6.5"/></svg>
<span>Added to cart</span>
</span>
<span aria-hidden="true" class="absolute left-7 top-1/2 -mt-[0.6rem] size-[1.2rem] opacity-0 pointer-events-none text-tb09-ink group-[.is-fly]:animate-tb09-fly motion-reduce:hidden">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-full"><path d="M3 4h2.2l2.3 10.5h9.8L19.5 7H6"/></svg>
</span>
<span aria-hidden="true" class="tb-09__badge absolute -top-2 -right-2 min-w-7 h-7 px-1.5 grid place-items-center rounded-full text-[0.8125rem] font-extrabold tabular-nums text-tb09-badge-ink bg-tb09-badge shadow-[0_2px_8px_-2px_var(--color-tb09-badge-glow)] [&.is-bump]:animate-tb09-bump motion-reduce:[&.is-bump]:animate-none motion-reduce:[&.is-bump]:outline-2 motion-reduce:[&.is-bump]:outline-tb09-badge motion-reduce:[&.is-bump]:outline-offset-2">2</span>
</button>
<span role="status" aria-live="polite" class="tb-09__live sr-only"></span>
</div>@import "tailwindcss";
@theme {
--color-tb09-bg: #f7f5f2;
--color-tb09-bg: oklch(0.97 0.006 85);
--color-tb09-accent: #111827;
--color-tb09-accent: oklch(0.2 0.015 265);
--color-tb09-ink: #ffffff;
--color-tb09-ok: #15803d;
--color-tb09-ok: oklch(0.52 0.14 148);
--color-tb09-badge: #f59e0b;
--color-tb09-badge: oklch(0.78 0.16 74);
--color-tb09-badge-ink: #1c1206;
--color-tb09-badge-ink: oklch(0.2 0.03 60);
--color-tb09-badge-glow: #f59e0bcc;
--color-tb09-badge-glow: oklch(0.78 0.16 74 / 0.8);
--color-tb09-glow: #111827d9;
--color-tb09-glow: oklch(0.2 0.015 265 / 0.85);
--font-tb09-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb09-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb09-fly: tb-09-fly 0.72s cubic-bezier(0.32, 0.06, 0.6, 1) forwards;
--animate-tb09-bump: tb-09-bump 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes tb-09-fly {
0% {
translate: 0 0;
scale: 1;
opacity: 1;
}
55% {
translate: calc(var(--tb-09-dx) * 0.6) calc(var(--tb-09-dy) * -1);
scale: 0.72;
opacity: 1;
}
100% {
translate: var(--tb-09-dx) -0.4rem;
scale: 0.25;
opacity: 0;
}
}
@keyframes tb-09-bump {
0% {
scale: 1;
}
35% {
scale: 1.35;
rotate: -6deg;
}
100% {
scale: 1;
rotate: 0deg;
}
}
.tb-09 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
@media (prefers-color-scheme: dark) {
.tb-09:not([data-theme="light"]) {
--color-tb09-bg: oklch(0.13 0.006 70);
--color-tb09-accent: oklch(0.95 0.005 85);
--color-tb09-ink: oklch(0.16 0.006 265);
--color-tb09-ok: oklch(0.72 0.17 148);
}
}
.tb-09[data-theme="dark"] {
--color-tb09-bg: oklch(0.13 0.006 70);
--color-tb09-accent: oklch(0.95 0.005 85);
--color-tb09-ink: oklch(0.16 0.006 265);
--color-tb09-ok: oklch(0.72 0.17 148);
}
@media (forced-colors: active) {
.tb-09 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-09__badge {
background: Highlight;
color: HighlightText;
}
}@import "tailwindcss";
@theme {
--color-tb09-bg: #f7f5f2;
--color-tb09-bg: oklch(0.97 0.006 85);
--color-tb09-accent: #111827;
--color-tb09-accent: oklch(0.2 0.015 265);
--color-tb09-ink: #ffffff;
--color-tb09-ok: #15803d;
--color-tb09-ok: oklch(0.52 0.14 148);
--color-tb09-badge: #f59e0b;
--color-tb09-badge: oklch(0.78 0.16 74);
--color-tb09-badge-ink: #1c1206;
--color-tb09-badge-ink: oklch(0.2 0.03 60);
--color-tb09-badge-glow: #f59e0bcc;
--color-tb09-badge-glow: oklch(0.78 0.16 74 / 0.8);
--color-tb09-glow: #111827d9;
--color-tb09-glow: oklch(0.2 0.015 265 / 0.85);
--font-tb09-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb09-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb09-fly: tb-09-fly 0.72s cubic-bezier(0.32, 0.06, 0.6, 1) forwards;
--animate-tb09-bump: tb-09-bump 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes tb-09-fly {
0% {
translate: 0 0;
scale: 1;
opacity: 1;
}
55% {
translate: calc(var(--tb-09-dx) * 0.6) calc(var(--tb-09-dy) * -1);
scale: 0.72;
opacity: 1;
}
100% {
translate: var(--tb-09-dx) -0.4rem;
scale: 0.25;
opacity: 0;
}
}
@keyframes tb-09-bump {
0% {
scale: 1;
}
35% {
scale: 1.35;
rotate: -6deg;
}
100% {
scale: 1;
rotate: 0deg;
}
}
.tb-09 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
@media (prefers-color-scheme: dark) {
.tb-09:not([data-theme="light"]) {
--color-tb09-bg: oklch(0.13 0.006 70);
--color-tb09-accent: oklch(0.95 0.005 85);
--color-tb09-ink: oklch(0.16 0.006 265);
--color-tb09-ok: oklch(0.72 0.17 148);
}
}
.tb-09[data-theme="dark"] {
--color-tb09-bg: oklch(0.13 0.006 70);
--color-tb09-accent: oklch(0.95 0.005 85);
--color-tb09-ink: oklch(0.16 0.006 265);
--color-tb09-ok: oklch(0.72 0.17 148);
}
@media (forced-colors: active) {
.tb-09 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-09__badge {
background: Highlight;
color: HighlightText;
}
}(() => {
const btn = document.querySelector('.tb-09__btn');
const badge = document.querySelector('.tb-09__badge');
const live = document.querySelector('.tb-09__live');
if (!btn) return;
let count = Number(badge.textContent) || 0;
let timer;
btn.addEventListener('click', () => {
count += 1;
btn.classList.remove('is-fly');
badge.classList.remove('is-bump');
void btn.offsetWidth; // force reflow so both animations restart
btn.classList.add('is-fly');
btn.dataset.state = 'done';
setTimeout(() => { badge.textContent = String(count); badge.classList.add('is-bump'); }, 380);
live.textContent = 'Added. Cart has ' + count + ' items.';
clearTimeout(timer);
timer = setTimeout(() => { btn.dataset.state = 'idle'; btn.classList.remove('is-fly'); }, 1600);
});
})();(() => {
const btn = document.querySelector('.tb-09__btn');
const badge = document.querySelector('.tb-09__badge');
const live = document.querySelector('.tb-09__live');
if (!btn) return;
let count = Number(badge.textContent) || 0;
let timer;
btn.addEventListener('click', () => {
count += 1;
btn.classList.remove('is-fly');
badge.classList.remove('is-bump');
void btn.offsetWidth; // force reflow so both animations restart
btn.classList.add('is-fly');
btn.dataset.state = 'done';
setTimeout(() => { badge.textContent = String(count); badge.classList.add('is-bump'); }, 380);
live.textContent = 'Added. Cart has ' + count + ' items.';
clearTimeout(timer);
timer = setTimeout(() => { btn.dataset.state = 'idle'; btn.classList.remove('is-fly'); }, 1600);
});
})();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-tb09-bg: #f7f5f2;
--color-tb09-bg: oklch(0.97 0.006 85);
--color-tb09-accent: #111827;
--color-tb09-accent: oklch(0.2 0.015 265);
--color-tb09-ink: #ffffff;
--color-tb09-ok: #15803d;
--color-tb09-ok: oklch(0.52 0.14 148);
--color-tb09-badge: #f59e0b;
--color-tb09-badge: oklch(0.78 0.16 74);
--color-tb09-badge-ink: #1c1206;
--color-tb09-badge-ink: oklch(0.2 0.03 60);
--color-tb09-badge-glow: #f59e0bcc;
--color-tb09-badge-glow: oklch(0.78 0.16 74 / 0.8);
--color-tb09-glow: #111827d9;
--color-tb09-glow: oklch(0.2 0.015 265 / 0.85);
--font-tb09-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb09-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb09-fly: tb-09-fly 0.72s cubic-bezier(0.32, 0.06, 0.6, 1) forwards;
--animate-tb09-bump: tb-09-bump 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes tb-09-fly {
0% {
translate: 0 0;
scale: 1;
opacity: 1;
}
55% {
translate: calc(var(--tb-09-dx) * 0.6) calc(var(--tb-09-dy) * -1);
scale: 0.72;
opacity: 1;
}
100% {
translate: var(--tb-09-dx) -0.4rem;
scale: 0.25;
opacity: 0;
}
}
@keyframes tb-09-bump {
0% {
scale: 1;
}
35% {
scale: 1.35;
rotate: -6deg;
}
100% {
scale: 1;
rotate: 0deg;
}
}
.tb-09 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
@media (prefers-color-scheme: dark) {
.tb-09:not([data-theme="light"]) {
--color-tb09-bg: oklch(0.13 0.006 70);
--color-tb09-accent: oklch(0.95 0.005 85);
--color-tb09-ink: oklch(0.16 0.006 265);
--color-tb09-ok: oklch(0.72 0.17 148);
}
}
.tb-09[data-theme="dark"] {
--color-tb09-bg: oklch(0.13 0.006 70);
--color-tb09-accent: oklch(0.95 0.005 85);
--color-tb09-ink: oklch(0.16 0.006 265);
--color-tb09-ok: oklch(0.72 0.17 148);
}
@media (forced-colors: active) {
.tb-09 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-09__badge {
background: Highlight;
color: HighlightText;
}
}@import "tailwindcss";
@theme {
--color-tb09-bg: #f7f5f2;
--color-tb09-bg: oklch(0.97 0.006 85);
--color-tb09-accent: #111827;
--color-tb09-accent: oklch(0.2 0.015 265);
--color-tb09-ink: #ffffff;
--color-tb09-ok: #15803d;
--color-tb09-ok: oklch(0.52 0.14 148);
--color-tb09-badge: #f59e0b;
--color-tb09-badge: oklch(0.78 0.16 74);
--color-tb09-badge-ink: #1c1206;
--color-tb09-badge-ink: oklch(0.2 0.03 60);
--color-tb09-badge-glow: #f59e0bcc;
--color-tb09-badge-glow: oklch(0.78 0.16 74 / 0.8);
--color-tb09-glow: #111827d9;
--color-tb09-glow: oklch(0.2 0.015 265 / 0.85);
--font-tb09-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb09-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb09-fly: tb-09-fly 0.72s cubic-bezier(0.32, 0.06, 0.6, 1) forwards;
--animate-tb09-bump: tb-09-bump 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes tb-09-fly {
0% {
translate: 0 0;
scale: 1;
opacity: 1;
}
55% {
translate: calc(var(--tb-09-dx) * 0.6) calc(var(--tb-09-dy) * -1);
scale: 0.72;
opacity: 1;
}
100% {
translate: var(--tb-09-dx) -0.4rem;
scale: 0.25;
opacity: 0;
}
}
@keyframes tb-09-bump {
0% {
scale: 1;
}
35% {
scale: 1.35;
rotate: -6deg;
}
100% {
scale: 1;
rotate: 0deg;
}
}
.tb-09 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
@media (prefers-color-scheme: dark) {
.tb-09:not([data-theme="light"]) {
--color-tb09-bg: oklch(0.13 0.006 70);
--color-tb09-accent: oklch(0.95 0.005 85);
--color-tb09-ink: oklch(0.16 0.006 265);
--color-tb09-ok: oklch(0.72 0.17 148);
}
}
.tb-09[data-theme="dark"] {
--color-tb09-bg: oklch(0.13 0.006 70);
--color-tb09-accent: oklch(0.95 0.005 85);
--color-tb09-ink: oklch(0.16 0.006 265);
--color-tb09-ok: oklch(0.72 0.17 148);
}
@media (forced-colors: active) {
.tb-09 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-09__badge {
background: Highlight;
color: HighlightText;
}
}<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=Plus+Jakarta+Sans:wght@500;600;700;800&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700;800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Utilities: both label slots, the badge, the lift/press, and the two animation hooks (
group-[.is-fly]:animate-tb09-flyand[&.is-bump]:animate-tb09-bump). CSS: the two keyframe blocks, because they interpolate custom properties across three stops. - The class-based group variant
group-[.is-fly]:is the right shape for a JS-driven one-shot: the controller toggles one class on the button, and the child animates. Nodata-attribute needed, and the reflow trick is unchanged. tabular-numson the badge is a small addition the CSS version made withfont-variant-numeric— identical output, one word.- A11y fix from the CSS original: the reduced-motion branch hid the flying glyph but kept the badge bump as an outline flash only; the port keeps the outline and guarantees the live-region text fires on the same tick, so the outcome is never motion-dependent.
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 Add to Cart Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-add-to-cart-button/
The most-measured button in e-commerce: press it and a cart glyph arcs into a badge, the digit bumps, and the label swaps to "Added" before returning to idle — with the real count announced politely, so screen-reader users hear the outcome instead of watching it.
## HTML
```html
<div class="tb-09 w-full min-h-svh grid place-items-center bg-tb09-bg font-tb09-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" data-state="idle" style="--tb-09-dx:6.5rem;--tb-09-dy:3.2rem" class="tb-09__btn group relative cursor-pointer grid items-center min-h-14 px-7 border-0 rounded-[0.85rem] text-base font-bold tracking-[-0.01em] text-tb09-ink bg-tb09-accent shadow-[0_12px_26px_-14px_var(--color-tb09-glow)] transition-[translate,background-color,scale] duration-200 ease-tb09-out data-[state=done]:bg-tb09-ok hover:-translate-y-0.5 active:translate-y-px active:scale-[0.99] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tb09-accent motion-reduce:transition-none motion-reduce:hover:translate-y-0">
<span class="col-start-1 row-start-1 flex items-center justify-center gap-2.5 opacity-0 scale-95 transition-[opacity,scale] duration-200 ease-tb09-out group-data-[state=idle]:opacity-100 group-data-[state=idle]:scale-100 motion-reduce:transition-none">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-[1.2rem] shrink-0"><path d="M3 4h2.2l2.3 10.5h9.8L19.5 7H6"/><circle cx="9.5" cy="19" r="1.5"/><circle cx="16.5" cy="19" r="1.5"/></svg>
<span>Add to cart — $148</span>
</span>
<span class="col-start-1 row-start-1 flex items-center justify-center gap-2.5 opacity-0 scale-95 transition-[opacity,scale] duration-200 ease-tb09-out group-data-[state=done]:opacity-100 group-data-[state=done]:scale-100 motion-reduce:transition-none">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" class="size-[1.2rem] shrink-0"><path d="M5 12.5 10 17.5 19 6.5"/></svg>
<span>Added to cart</span>
</span>
<span aria-hidden="true" class="absolute left-7 top-1/2 -mt-[0.6rem] size-[1.2rem] opacity-0 pointer-events-none text-tb09-ink group-[.is-fly]:animate-tb09-fly motion-reduce:hidden">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-full"><path d="M3 4h2.2l2.3 10.5h9.8L19.5 7H6"/></svg>
</span>
<span aria-hidden="true" class="tb-09__badge absolute -top-2 -right-2 min-w-7 h-7 px-1.5 grid place-items-center rounded-full text-[0.8125rem] font-extrabold tabular-nums text-tb09-badge-ink bg-tb09-badge shadow-[0_2px_8px_-2px_var(--color-tb09-badge-glow)] [&.is-bump]:animate-tb09-bump motion-reduce:[&.is-bump]:animate-none motion-reduce:[&.is-bump]:outline-2 motion-reduce:[&.is-bump]:outline-tb09-badge motion-reduce:[&.is-bump]:outline-offset-2">2</span>
</button>
<span role="status" aria-live="polite" class="tb-09__live sr-only"></span>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb09-bg: #f7f5f2;
--color-tb09-bg: oklch(0.97 0.006 85);
--color-tb09-accent: #111827;
--color-tb09-accent: oklch(0.2 0.015 265);
--color-tb09-ink: #ffffff;
--color-tb09-ok: #15803d;
--color-tb09-ok: oklch(0.52 0.14 148);
--color-tb09-badge: #f59e0b;
--color-tb09-badge: oklch(0.78 0.16 74);
--color-tb09-badge-ink: #1c1206;
--color-tb09-badge-ink: oklch(0.2 0.03 60);
--color-tb09-badge-glow: #f59e0bcc;
--color-tb09-badge-glow: oklch(0.78 0.16 74 / 0.8);
--color-tb09-glow: #111827d9;
--color-tb09-glow: oklch(0.2 0.015 265 / 0.85);
--font-tb09-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb09-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb09-fly: tb-09-fly 0.72s cubic-bezier(0.32, 0.06, 0.6, 1) forwards;
--animate-tb09-bump: tb-09-bump 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes tb-09-fly {
0% {
translate: 0 0;
scale: 1;
opacity: 1;
}
55% {
translate: calc(var(--tb-09-dx) * 0.6) calc(var(--tb-09-dy) * -1);
scale: 0.72;
opacity: 1;
}
100% {
translate: var(--tb-09-dx) -0.4rem;
scale: 0.25;
opacity: 0;
}
}
@keyframes tb-09-bump {
0% {
scale: 1;
}
35% {
scale: 1.35;
rotate: -6deg;
}
100% {
scale: 1;
rotate: 0deg;
}
}
.tb-09 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
@media (prefers-color-scheme: dark) {
.tb-09:not([data-theme="light"]) {
--color-tb09-bg: oklch(0.13 0.006 70);
--color-tb09-accent: oklch(0.95 0.005 85);
--color-tb09-ink: oklch(0.16 0.006 265);
--color-tb09-ok: oklch(0.72 0.17 148);
}
}
.tb-09[data-theme="dark"] {
--color-tb09-bg: oklch(0.13 0.006 70);
--color-tb09-accent: oklch(0.95 0.005 85);
--color-tb09-ink: oklch(0.16 0.006 265);
--color-tb09-ok: oklch(0.72 0.17 148);
}
@media (forced-colors: active) {
.tb-09 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-09__badge {
background: Highlight;
color: HighlightText;
}
}
```
## JavaScript
```js
(() => {
const btn = document.querySelector('.tb-09__btn');
const badge = document.querySelector('.tb-09__badge');
const live = document.querySelector('.tb-09__live');
if (!btn) return;
let count = Number(badge.textContent) || 0;
let timer;
btn.addEventListener('click', () => {
count += 1;
btn.classList.remove('is-fly');
badge.classList.remove('is-bump');
void btn.offsetWidth; // force reflow so both animations restart
btn.classList.add('is-fly');
btn.dataset.state = 'done';
setTimeout(() => { badge.textContent = String(count); badge.classList.add('is-bump'); }, 380);
live.textContent = 'Added. Cart has ' + count + ' items.';
clearTimeout(timer);
timer = setTimeout(() => { btn.dataset.state = 'idle'; btn.classList.remove('is-fly'); }, 1600);
});
})();
```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 Add to Cart Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-add-to-cart-button/
The most-measured button in e-commerce: press it and a cart glyph arcs into a badge, the digit bumps, and the label swaps to "Added" before returning to idle — with the real count announced politely, so screen-reader users hear the outcome instead of watching it.
## HTML
```html
<div class="tb-09 w-full min-h-svh grid place-items-center bg-tb09-bg font-tb09-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" data-state="idle" style="--tb-09-dx:6.5rem;--tb-09-dy:3.2rem" class="tb-09__btn group relative cursor-pointer grid items-center min-h-14 px-7 border-0 rounded-[0.85rem] text-base font-bold tracking-[-0.01em] text-tb09-ink bg-tb09-accent shadow-[0_12px_26px_-14px_var(--color-tb09-glow)] transition-[translate,background-color,scale] duration-200 ease-tb09-out data-[state=done]:bg-tb09-ok hover:-translate-y-0.5 active:translate-y-px active:scale-[0.99] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tb09-accent motion-reduce:transition-none motion-reduce:hover:translate-y-0">
<span class="col-start-1 row-start-1 flex items-center justify-center gap-2.5 opacity-0 scale-95 transition-[opacity,scale] duration-200 ease-tb09-out group-data-[state=idle]:opacity-100 group-data-[state=idle]:scale-100 motion-reduce:transition-none">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-[1.2rem] shrink-0"><path d="M3 4h2.2l2.3 10.5h9.8L19.5 7H6"/><circle cx="9.5" cy="19" r="1.5"/><circle cx="16.5" cy="19" r="1.5"/></svg>
<span>Add to cart — $148</span>
</span>
<span class="col-start-1 row-start-1 flex items-center justify-center gap-2.5 opacity-0 scale-95 transition-[opacity,scale] duration-200 ease-tb09-out group-data-[state=done]:opacity-100 group-data-[state=done]:scale-100 motion-reduce:transition-none">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" class="size-[1.2rem] shrink-0"><path d="M5 12.5 10 17.5 19 6.5"/></svg>
<span>Added to cart</span>
</span>
<span aria-hidden="true" class="absolute left-7 top-1/2 -mt-[0.6rem] size-[1.2rem] opacity-0 pointer-events-none text-tb09-ink group-[.is-fly]:animate-tb09-fly motion-reduce:hidden">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-full"><path d="M3 4h2.2l2.3 10.5h9.8L19.5 7H6"/></svg>
</span>
<span aria-hidden="true" class="tb-09__badge absolute -top-2 -right-2 min-w-7 h-7 px-1.5 grid place-items-center rounded-full text-[0.8125rem] font-extrabold tabular-nums text-tb09-badge-ink bg-tb09-badge shadow-[0_2px_8px_-2px_var(--color-tb09-badge-glow)] [&.is-bump]:animate-tb09-bump motion-reduce:[&.is-bump]:animate-none motion-reduce:[&.is-bump]:outline-2 motion-reduce:[&.is-bump]:outline-tb09-badge motion-reduce:[&.is-bump]:outline-offset-2">2</span>
</button>
<span role="status" aria-live="polite" class="tb-09__live sr-only"></span>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb09-bg: #f7f5f2;
--color-tb09-bg: oklch(0.97 0.006 85);
--color-tb09-accent: #111827;
--color-tb09-accent: oklch(0.2 0.015 265);
--color-tb09-ink: #ffffff;
--color-tb09-ok: #15803d;
--color-tb09-ok: oklch(0.52 0.14 148);
--color-tb09-badge: #f59e0b;
--color-tb09-badge: oklch(0.78 0.16 74);
--color-tb09-badge-ink: #1c1206;
--color-tb09-badge-ink: oklch(0.2 0.03 60);
--color-tb09-badge-glow: #f59e0bcc;
--color-tb09-badge-glow: oklch(0.78 0.16 74 / 0.8);
--color-tb09-glow: #111827d9;
--color-tb09-glow: oklch(0.2 0.015 265 / 0.85);
--font-tb09-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb09-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb09-fly: tb-09-fly 0.72s cubic-bezier(0.32, 0.06, 0.6, 1) forwards;
--animate-tb09-bump: tb-09-bump 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes tb-09-fly {
0% {
translate: 0 0;
scale: 1;
opacity: 1;
}
55% {
translate: calc(var(--tb-09-dx) * 0.6) calc(var(--tb-09-dy) * -1);
scale: 0.72;
opacity: 1;
}
100% {
translate: var(--tb-09-dx) -0.4rem;
scale: 0.25;
opacity: 0;
}
}
@keyframes tb-09-bump {
0% {
scale: 1;
}
35% {
scale: 1.35;
rotate: -6deg;
}
100% {
scale: 1;
rotate: 0deg;
}
}
.tb-09 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
}
@media (prefers-color-scheme: dark) {
.tb-09:not([data-theme="light"]) {
--color-tb09-bg: oklch(0.13 0.006 70);
--color-tb09-accent: oklch(0.95 0.005 85);
--color-tb09-ink: oklch(0.16 0.006 265);
--color-tb09-ok: oklch(0.72 0.17 148);
}
}
.tb-09[data-theme="dark"] {
--color-tb09-bg: oklch(0.13 0.006 70);
--color-tb09-accent: oklch(0.95 0.005 85);
--color-tb09-ink: oklch(0.16 0.006 265);
--color-tb09-ok: oklch(0.72 0.17 148);
}
@media (forced-colors: active) {
.tb-09 button {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
}
.tb-09__badge {
background: Highlight;
color: HighlightText;
}
}
```
## JavaScript
```js
(() => {
const btn = document.querySelector('.tb-09__btn');
const badge = document.querySelector('.tb-09__badge');
const live = document.querySelector('.tb-09__live');
if (!btn) return;
let count = Number(badge.textContent) || 0;
let timer;
btn.addEventListener('click', () => {
count += 1;
btn.classList.remove('is-fly');
badge.classList.remove('is-bump');
void btn.offsetWidth; // force reflow so both animations restart
btn.classList.add('is-fly');
btn.dataset.state = 'done';
setTimeout(() => { badge.textContent = String(count); badge.classList.add('is-bump'); }, 380);
live.textContent = 'Added. Cart has ' + count + ' items.';
clearTimeout(timer);
timer = setTimeout(() => { btn.dataset.state = 'idle'; btn.classList.remove('is-fly'); }, 1600);
});
})();
```How this works
The arc is a keyframe with tokens, not a utility. Two stacked transforms make a parabola without offset-path, and the travel distance stays a pair of custom properties on the button so JS can retarget it at a real header cart:
@keyframes tb-09-fly{
0% { translate:0 0; scale:1; opacity:1; }
55% { translate:calc(var(--tb-09-dx) * .6) calc(var(--tb-09-dy) * -1); scale:.72; }
100%{ translate:var(--tb-09-dx) -.4rem; scale:.25; opacity:0; }
}Retriggering still needs a reflow. Remove the class, read offsetWidth, add it back — Tailwind changes nothing here, and the comment explaining why stays in the JS.
The announcement is the feature. "Added. Cart has 3 items." goes into an aria-live="polite" span (the sr-only utility). If reduced motion removes the arc entirely, the outcome is unaffected — which is the test every animated button should pass.
Make it yours
- Retarget the arc by editing
--tb-09-dx/--tb-09-dyin the style attribute, or set them from JS against a measured header cart. - Remove the 1600ms reset entirely if your cart drawer opens on add.
- Swap the badge for a mini-cart preview — it is absolutely positioned at the same anchor.
- Quantity stepper: put the button in a flex row; the internal grid is unaffected.
- Out of stock:
aria-disabled="true"plus thearia-disabled:utilities from demo 07.
Gotchas — read before shipping
- Re-triggering a CSS animation needs a forced reflow between class removal and re-add, or the browser coalesces both changes and nothing plays.
- Never let the animation be the only success signal — under
motion-reduce:the text swap and live region must still fire. - Don't disable the button during the animation; shoppers legitimately add two of the same item, and a locked button reads as a failure.
aria-live="assertive"interrupts mid-sentence. Cart updates are polite.- An
overflow-hiddenancestor clips the glyph mid-arc. The button here isrelativewithout clipping for exactly that reason.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Individual transform properties, grid stacking, keyframes and aria-live are all well below Tailwind v4's own floor.