13 / 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 Download Button with Progress Fill
Click and the arrow bounces, a progress bar sweeps the button's bottom edge, and the label resolves to "Downloaded!". Three states, one data-state attribute, and a progress bar that is a single after: layer scaling on the compositor.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tab-13 w-full min-h-svh grid place-items-center bg-tab13-bg font-tab13-sans px-6 py-10">
<div class="flex flex-col items-center gap-5">
<p class="text-[0.78rem] tracking-[0.1em] uppercase text-tab13-mut m-0">Click the button to download</p>
<button type="button" id="tab13-btn" data-state="idle" class="relative overflow-hidden inline-flex items-center gap-2.5 min-h-11 px-9 text-base font-bold text-white bg-tab13-blue rounded-xl cursor-pointer shadow-[0_4px_16px_oklch(0.55_0.22_264/0.4)] transition-[background-color,box-shadow,translate] duration-300 hover:brightness-108 hover:-translate-y-0.5 active:translate-y-0 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab13-blue data-[state=downloading]:bg-tab13-deep data-[state=downloading]:cursor-not-allowed data-[state=downloading]:hover:translate-y-0 data-[state=done]:bg-tab13-done data-[state=done]:shadow-[0_4px_16px_oklch(0.58_0.14_163/0.4)] after:content-[''] after:absolute after:left-0 after:bottom-0 after:w-full after:h-[3px] after:bg-white/55 after:origin-left after:scale-x-0 after:pointer-events-none after:transition-transform after:duration-[2200ms] after:ease-linear data-[state=downloading]:after:scale-x-100 data-[state=downloading]:[&_svg]:animate-tab13-bounce motion-reduce:transition-none motion-reduce:after:transition-none motion-reduce:data-[state=downloading]:[&_svg]:animate-none">
<svg id="tab13-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="size-5 shrink-0">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
<span id="tab13-text" role="status" aria-live="polite">Download v2.4.1</span>
</button>
<p class="text-[0.78rem] tracking-[0.1em] uppercase text-tab13-mut m-0">macOS · 48.2 MB</p>
</div>
</section><section class="tab-13 w-full min-h-svh grid place-items-center bg-tab13-bg font-tab13-sans px-6 py-10">
<div class="flex flex-col items-center gap-5">
<p class="text-[0.78rem] tracking-[0.1em] uppercase text-tab13-mut m-0">Click the button to download</p>
<button type="button" id="tab13-btn" data-state="idle" class="relative overflow-hidden inline-flex items-center gap-2.5 min-h-11 px-9 text-base font-bold text-white bg-tab13-blue rounded-xl cursor-pointer shadow-[0_4px_16px_oklch(0.55_0.22_264/0.4)] transition-[background-color,box-shadow,translate] duration-300 hover:brightness-108 hover:-translate-y-0.5 active:translate-y-0 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab13-blue data-[state=downloading]:bg-tab13-deep data-[state=downloading]:cursor-not-allowed data-[state=downloading]:hover:translate-y-0 data-[state=done]:bg-tab13-done data-[state=done]:shadow-[0_4px_16px_oklch(0.58_0.14_163/0.4)] after:content-[''] after:absolute after:left-0 after:bottom-0 after:w-full after:h-[3px] after:bg-white/55 after:origin-left after:scale-x-0 after:pointer-events-none after:transition-transform after:duration-[2200ms] after:ease-linear data-[state=downloading]:after:scale-x-100 data-[state=downloading]:[&_svg]:animate-tab13-bounce motion-reduce:transition-none motion-reduce:after:transition-none motion-reduce:data-[state=downloading]:[&_svg]:animate-none">
<svg id="tab13-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="size-5 shrink-0">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
<span id="tab13-text" role="status" aria-live="polite">Download v2.4.1</span>
</button>
<p class="text-[0.78rem] tracking-[0.1em] uppercase text-tab13-mut m-0">macOS · 48.2 MB</p>
</div>
</section>@import "tailwindcss";
@theme {
--color-tab13-bg: #f0f4ff;
--color-tab13-bg: oklch(0.97 0.015 265);
--color-tab13-mut: #64748b;
--color-tab13-mut: oklch(0.55 0.03 255);
--color-tab13-blue: #2563eb;
--color-tab13-blue: oklch(0.55 0.22 264);
--color-tab13-deep: #1d4ed8;
--color-tab13-deep: oklch(0.5 0.22 264);
--color-tab13-done: #059669;
--color-tab13-done: oklch(0.58 0.14 163);
--font-tab13-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab13-bounce: tab13-bounce 0.6s ease-in-out infinite;
}
@keyframes tab13-bounce {
0%,
100% {
translate: 0 0;
}
50% {
translate: 0 4px;
}
}
.tab-13 {
width: 100%;
min-height: 100svh;
}
.tab-13 ::selection {
background: var(--color-tab13-blue);
color: #fff;
}@import "tailwindcss";
@theme {
--color-tab13-bg: #f0f4ff;
--color-tab13-bg: oklch(0.97 0.015 265);
--color-tab13-mut: #64748b;
--color-tab13-mut: oklch(0.55 0.03 255);
--color-tab13-blue: #2563eb;
--color-tab13-blue: oklch(0.55 0.22 264);
--color-tab13-deep: #1d4ed8;
--color-tab13-deep: oklch(0.5 0.22 264);
--color-tab13-done: #059669;
--color-tab13-done: oklch(0.58 0.14 163);
--font-tab13-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab13-bounce: tab13-bounce 0.6s ease-in-out infinite;
}
@keyframes tab13-bounce {
0%,
100% {
translate: 0 0;
}
50% {
translate: 0 4px;
}
}
.tab-13 {
width: 100%;
min-height: 100svh;
}
.tab-13 ::selection {
background: var(--color-tab13-blue);
color: #fff;
}(function () {
var btn = document.getElementById('tab13-btn');
var txt = document.getElementById('tab13-text');
var icon = document.getElementById('tab13-icon');
if (!btn || !txt || !icon) return;
var DONE_ICON = '<polyline points="20 6 9 17 4 12"/>';
var IDLE_ICON = icon.innerHTML;
btn.addEventListener('click', function () {
if (btn.dataset.state !== 'idle') return;
btn.dataset.state = 'downloading';
btn.setAttribute('aria-busy', 'true');
txt.textContent = 'Downloading\u2026';
setTimeout(function () {
btn.dataset.state = 'done';
btn.setAttribute('aria-busy', 'false');
icon.innerHTML = DONE_ICON;
txt.textContent = 'Downloaded!';
setTimeout(function () {
btn.dataset.state = 'idle';
icon.innerHTML = IDLE_ICON;
txt.textContent = 'Download v2.4.1';
}, 2000);
}, 2200);
});
})();(function () {
var btn = document.getElementById('tab13-btn');
var txt = document.getElementById('tab13-text');
var icon = document.getElementById('tab13-icon');
if (!btn || !txt || !icon) return;
var DONE_ICON = '<polyline points="20 6 9 17 4 12"/>';
var IDLE_ICON = icon.innerHTML;
btn.addEventListener('click', function () {
if (btn.dataset.state !== 'idle') return;
btn.dataset.state = 'downloading';
btn.setAttribute('aria-busy', 'true');
txt.textContent = 'Downloading\u2026';
setTimeout(function () {
btn.dataset.state = 'done';
btn.setAttribute('aria-busy', 'false');
icon.innerHTML = DONE_ICON;
txt.textContent = 'Downloaded!';
setTimeout(function () {
btn.dataset.state = 'idle';
icon.innerHTML = IDLE_ICON;
txt.textContent = 'Download v2.4.1';
}, 2000);
}, 2200);
});
})();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-tab13-bg: #f0f4ff;
--color-tab13-bg: oklch(0.97 0.015 265);
--color-tab13-mut: #64748b;
--color-tab13-mut: oklch(0.55 0.03 255);
--color-tab13-blue: #2563eb;
--color-tab13-blue: oklch(0.55 0.22 264);
--color-tab13-deep: #1d4ed8;
--color-tab13-deep: oklch(0.5 0.22 264);
--color-tab13-done: #059669;
--color-tab13-done: oklch(0.58 0.14 163);
--font-tab13-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab13-bounce: tab13-bounce 0.6s ease-in-out infinite;
}
@keyframes tab13-bounce {
0%,
100% {
translate: 0 0;
}
50% {
translate: 0 4px;
}
}
.tab-13 {
width: 100%;
min-height: 100svh;
}
.tab-13 ::selection {
background: var(--color-tab13-blue);
color: #fff;
}@import "tailwindcss";
@theme {
--color-tab13-bg: #f0f4ff;
--color-tab13-bg: oklch(0.97 0.015 265);
--color-tab13-mut: #64748b;
--color-tab13-mut: oklch(0.55 0.03 255);
--color-tab13-blue: #2563eb;
--color-tab13-blue: oklch(0.55 0.22 264);
--color-tab13-deep: #1d4ed8;
--color-tab13-deep: oklch(0.5 0.22 264);
--color-tab13-done: #059669;
--color-tab13-done: oklch(0.58 0.14 163);
--font-tab13-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab13-bounce: tab13-bounce 0.6s ease-in-out infinite;
}
@keyframes tab13-bounce {
0%,
100% {
translate: 0 0;
}
50% {
translate: 0 4px;
}
}
.tab-13 {
width: 100%;
min-height: 100svh;
}
.tab-13 ::selection {
background: var(--color-tab13-blue);
color: #fff;
}<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.
[&_svg]:is how a state on the button reaches the icon without giving the icon a class of its own — the arbitrary-variant escape hatch used for exactly what it is for.- Two independent classes become one state value, which removes the double-click race the CSS version could reach.
- UI change from the CSS original: the label is an
aria-liveregion and the button reportsaria-busywhile fetching, so the progress bar is not the only channel.
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 Download Button with Progress Fill
Source: https://codefronts.com/motion/tailwind-css-animated-buttons/tailwind-animated-download-button/
Click and the arrow bounces, a progress bar sweeps the button's bottom edge, and the label resolves to "Downloaded!". Three states, one data-state attribute, and a progress bar that is a single after: layer scaling on the compositor.
## HTML
```html
<section class="tab-13 w-full min-h-svh grid place-items-center bg-tab13-bg font-tab13-sans px-6 py-10">
<div class="flex flex-col items-center gap-5">
<p class="text-[0.78rem] tracking-[0.1em] uppercase text-tab13-mut m-0">Click the button to download</p>
<button type="button" id="tab13-btn" data-state="idle" class="relative overflow-hidden inline-flex items-center gap-2.5 min-h-11 px-9 text-base font-bold text-white bg-tab13-blue rounded-xl cursor-pointer shadow-[0_4px_16px_oklch(0.55_0.22_264/0.4)] transition-[background-color,box-shadow,translate] duration-300 hover:brightness-108 hover:-translate-y-0.5 active:translate-y-0 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab13-blue data-[state=downloading]:bg-tab13-deep data-[state=downloading]:cursor-not-allowed data-[state=downloading]:hover:translate-y-0 data-[state=done]:bg-tab13-done data-[state=done]:shadow-[0_4px_16px_oklch(0.58_0.14_163/0.4)] after:content-[''] after:absolute after:left-0 after:bottom-0 after:w-full after:h-[3px] after:bg-white/55 after:origin-left after:scale-x-0 after:pointer-events-none after:transition-transform after:duration-[2200ms] after:ease-linear data-[state=downloading]:after:scale-x-100 data-[state=downloading]:[&_svg]:animate-tab13-bounce motion-reduce:transition-none motion-reduce:after:transition-none motion-reduce:data-[state=downloading]:[&_svg]:animate-none">
<svg id="tab13-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="size-5 shrink-0">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
<span id="tab13-text" role="status" aria-live="polite">Download v2.4.1</span>
</button>
<p class="text-[0.78rem] tracking-[0.1em] uppercase text-tab13-mut m-0">macOS · 48.2 MB</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tab13-bg: #f0f4ff;
--color-tab13-bg: oklch(0.97 0.015 265);
--color-tab13-mut: #64748b;
--color-tab13-mut: oklch(0.55 0.03 255);
--color-tab13-blue: #2563eb;
--color-tab13-blue: oklch(0.55 0.22 264);
--color-tab13-deep: #1d4ed8;
--color-tab13-deep: oklch(0.5 0.22 264);
--color-tab13-done: #059669;
--color-tab13-done: oklch(0.58 0.14 163);
--font-tab13-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab13-bounce: tab13-bounce 0.6s ease-in-out infinite;
}
@keyframes tab13-bounce {
0%,
100% {
translate: 0 0;
}
50% {
translate: 0 4px;
}
}
.tab-13 {
width: 100%;
min-height: 100svh;
}
.tab-13 ::selection {
background: var(--color-tab13-blue);
color: #fff;
}
```
## JavaScript
```js
(function () {
var btn = document.getElementById('tab13-btn');
var txt = document.getElementById('tab13-text');
var icon = document.getElementById('tab13-icon');
if (!btn || !txt || !icon) return;
var DONE_ICON = '<polyline points="20 6 9 17 4 12"/>';
var IDLE_ICON = icon.innerHTML;
btn.addEventListener('click', function () {
if (btn.dataset.state !== 'idle') return;
btn.dataset.state = 'downloading';
btn.setAttribute('aria-busy', 'true');
txt.textContent = 'Downloading\u2026';
setTimeout(function () {
btn.dataset.state = 'done';
btn.setAttribute('aria-busy', 'false');
icon.innerHTML = DONE_ICON;
txt.textContent = 'Downloaded!';
setTimeout(function () {
btn.dataset.state = 'idle';
icon.innerHTML = IDLE_ICON;
txt.textContent = 'Download v2.4.1';
}, 2000);
}, 2200);
});
})();
```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 Download Button with Progress Fill
Source: https://codefronts.com/motion/tailwind-css-animated-buttons/tailwind-animated-download-button/
Click and the arrow bounces, a progress bar sweeps the button's bottom edge, and the label resolves to "Downloaded!". Three states, one data-state attribute, and a progress bar that is a single after: layer scaling on the compositor.
## HTML
```html
<section class="tab-13 w-full min-h-svh grid place-items-center bg-tab13-bg font-tab13-sans px-6 py-10">
<div class="flex flex-col items-center gap-5">
<p class="text-[0.78rem] tracking-[0.1em] uppercase text-tab13-mut m-0">Click the button to download</p>
<button type="button" id="tab13-btn" data-state="idle" class="relative overflow-hidden inline-flex items-center gap-2.5 min-h-11 px-9 text-base font-bold text-white bg-tab13-blue rounded-xl cursor-pointer shadow-[0_4px_16px_oklch(0.55_0.22_264/0.4)] transition-[background-color,box-shadow,translate] duration-300 hover:brightness-108 hover:-translate-y-0.5 active:translate-y-0 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab13-blue data-[state=downloading]:bg-tab13-deep data-[state=downloading]:cursor-not-allowed data-[state=downloading]:hover:translate-y-0 data-[state=done]:bg-tab13-done data-[state=done]:shadow-[0_4px_16px_oklch(0.58_0.14_163/0.4)] after:content-[''] after:absolute after:left-0 after:bottom-0 after:w-full after:h-[3px] after:bg-white/55 after:origin-left after:scale-x-0 after:pointer-events-none after:transition-transform after:duration-[2200ms] after:ease-linear data-[state=downloading]:after:scale-x-100 data-[state=downloading]:[&_svg]:animate-tab13-bounce motion-reduce:transition-none motion-reduce:after:transition-none motion-reduce:data-[state=downloading]:[&_svg]:animate-none">
<svg id="tab13-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="size-5 shrink-0">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
<span id="tab13-text" role="status" aria-live="polite">Download v2.4.1</span>
</button>
<p class="text-[0.78rem] tracking-[0.1em] uppercase text-tab13-mut m-0">macOS · 48.2 MB</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tab13-bg: #f0f4ff;
--color-tab13-bg: oklch(0.97 0.015 265);
--color-tab13-mut: #64748b;
--color-tab13-mut: oklch(0.55 0.03 255);
--color-tab13-blue: #2563eb;
--color-tab13-blue: oklch(0.55 0.22 264);
--color-tab13-deep: #1d4ed8;
--color-tab13-deep: oklch(0.5 0.22 264);
--color-tab13-done: #059669;
--color-tab13-done: oklch(0.58 0.14 163);
--font-tab13-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab13-bounce: tab13-bounce 0.6s ease-in-out infinite;
}
@keyframes tab13-bounce {
0%,
100% {
translate: 0 0;
}
50% {
translate: 0 4px;
}
}
.tab-13 {
width: 100%;
min-height: 100svh;
}
.tab-13 ::selection {
background: var(--color-tab13-blue);
color: #fff;
}
```
## JavaScript
```js
(function () {
var btn = document.getElementById('tab13-btn');
var txt = document.getElementById('tab13-text');
var icon = document.getElementById('tab13-icon');
if (!btn || !txt || !icon) return;
var DONE_ICON = '<polyline points="20 6 9 17 4 12"/>';
var IDLE_ICON = icon.innerHTML;
btn.addEventListener('click', function () {
if (btn.dataset.state !== 'idle') return;
btn.dataset.state = 'downloading';
btn.setAttribute('aria-busy', 'true');
txt.textContent = 'Downloading\u2026';
setTimeout(function () {
btn.dataset.state = 'done';
btn.setAttribute('aria-busy', 'false');
icon.innerHTML = DONE_ICON;
txt.textContent = 'Downloaded!';
setTimeout(function () {
btn.dataset.state = 'idle';
icon.innerHTML = IDLE_ICON;
txt.textContent = 'Download v2.4.1';
}, 2000);
}, 2200);
});
})();
```How this works
The progress bar is a transition, not an animation. after:scale-x-0 at rest, scale-x-100 while downloading, with a 2200ms linear transition — the exact duration of the simulated request. One property, compositor-only, and it lands precisely when the controller flips to done.
Three states cannot overlap. The CSS original toggled .is-downloading and .is-done independently, which made "both classes present" reachable through a fast double click. A single data-state value makes that state unrepresentable — the port fixes a bug, not just a syntax.
The icon swap stays in JS. Replacing a download glyph with a checkmark is content, not style; the controller swaps the SVG's inner markup and restores it on reset.
Make it yours
- Duration: change the controller's
2200and the matchingafter:duration-[2200ms]together — they are one number in two places by necessity. - Progress colour:
after:bg-white/55, or a token if you need it themeable. - Swap the linear bar for a ring by animating
stroke-dashoffseton an SVG circle instead of theafter:layer.
Gotchas — read before shipping
- The transition only fires if the state flips on a later frame than the initial paint. Setting
data-stateinside a click handler satisfies that; setting it during initial render does not. after:pointer-events-nonekeeps the fill from intercepting the click that would cancel a download.- Do not animate the
widthof the bar.scale-xwithorigin-leftlooks identical and costs nothing per frame.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Scale transitions and SVG swaps are universally supported; the floor is Tailwind v4's generated sheet.