12 / 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 Submit Button Success State with Checkmark Draw
A submit button that collapses into a circle and draws a checkmark. The port replaces a class toggle with data-state, so the markup reads data-[state=success]:w-12 and the impossible state ("success and idle at once") stops existing.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tab-12 w-full min-h-svh grid place-items-center bg-tab12-bg font-tab12-sans px-6 py-10">
<div class="w-[min(380px,100%)] p-8 bg-white rounded-2xl shadow-[0_4px_24px_oklch(0_0_0/0.08)]">
<h2 class="text-[1.4rem] font-bold text-tab12-ink m-0">Contact Us</h2>
<p class="mt-1.5 mb-6 text-[0.88rem] leading-relaxed text-tab12-mut">We'll get back to you within 24 hours.</p>
<label class="block mb-3.5">
<span class="sr-only">Your name</span>
<input type="text" placeholder="Your name" class="w-full min-h-11 px-4 text-[0.9rem] text-tab12-ink bg-tab12-bg border-[1.5px] border-tab12-edge rounded-lg transition-colors duration-200 focus:border-tab12-primary focus:outline-2 focus:outline-offset-2 focus:outline-tab12-primary/40">
</label>
<label class="block mb-4">
<span class="sr-only">Email address</span>
<input type="email" placeholder="Email address" class="w-full min-h-11 px-4 text-[0.9rem] text-tab12-ink bg-tab12-bg border-[1.5px] border-tab12-edge rounded-lg transition-colors duration-200 focus:border-tab12-primary focus:outline-2 focus:outline-offset-2 focus:outline-tab12-primary/40">
</label>
<div class="flex justify-center">
<button type="button" id="tab12-btn" data-state="idle" class="relative flex items-center justify-center w-full h-12 px-6 text-base font-bold text-white bg-tab12-primary rounded-full cursor-pointer overflow-hidden shadow-[0_4px_16px_oklch(0.51_0.24_277/0.3)] transition-[width,background-color,box-shadow] duration-[400ms] ease-[cubic-bezier(0.4,0,0.2,1)] hover:brightness-108 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab12-primary data-[state=success]:w-12 data-[state=success]:bg-tab12-success data-[state=success]:shadow-[0_4px_16px_oklch(0.7_0.15_163/0.35)] data-[state=success]:cursor-default motion-reduce:transition-none">
<span class="whitespace-nowrap transition-[opacity,scale] duration-200">Send Message</span>
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="absolute size-6 opacity-0 scale-50 transition-[opacity,scale] duration-300 delay-[350ms]">
<polyline points="20 6 9 17 4 12" pathLength="30" stroke-dasharray="30" stroke-dashoffset="30"></polyline>
</svg>
</button>
</div>
<p id="tab12-status" role="status" aria-live="polite" class="mt-3 text-center text-[0.8rem] text-tab12-mut min-h-5"></p>
</div>
</section><section class="tab-12 w-full min-h-svh grid place-items-center bg-tab12-bg font-tab12-sans px-6 py-10">
<div class="w-[min(380px,100%)] p-8 bg-white rounded-2xl shadow-[0_4px_24px_oklch(0_0_0/0.08)]">
<h2 class="text-[1.4rem] font-bold text-tab12-ink m-0">Contact Us</h2>
<p class="mt-1.5 mb-6 text-[0.88rem] leading-relaxed text-tab12-mut">We'll get back to you within 24 hours.</p>
<label class="block mb-3.5">
<span class="sr-only">Your name</span>
<input type="text" placeholder="Your name" class="w-full min-h-11 px-4 text-[0.9rem] text-tab12-ink bg-tab12-bg border-[1.5px] border-tab12-edge rounded-lg transition-colors duration-200 focus:border-tab12-primary focus:outline-2 focus:outline-offset-2 focus:outline-tab12-primary/40">
</label>
<label class="block mb-4">
<span class="sr-only">Email address</span>
<input type="email" placeholder="Email address" class="w-full min-h-11 px-4 text-[0.9rem] text-tab12-ink bg-tab12-bg border-[1.5px] border-tab12-edge rounded-lg transition-colors duration-200 focus:border-tab12-primary focus:outline-2 focus:outline-offset-2 focus:outline-tab12-primary/40">
</label>
<div class="flex justify-center">
<button type="button" id="tab12-btn" data-state="idle" class="relative flex items-center justify-center w-full h-12 px-6 text-base font-bold text-white bg-tab12-primary rounded-full cursor-pointer overflow-hidden shadow-[0_4px_16px_oklch(0.51_0.24_277/0.3)] transition-[width,background-color,box-shadow] duration-[400ms] ease-[cubic-bezier(0.4,0,0.2,1)] hover:brightness-108 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab12-primary data-[state=success]:w-12 data-[state=success]:bg-tab12-success data-[state=success]:shadow-[0_4px_16px_oklch(0.7_0.15_163/0.35)] data-[state=success]:cursor-default motion-reduce:transition-none">
<span class="whitespace-nowrap transition-[opacity,scale] duration-200">Send Message</span>
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="absolute size-6 opacity-0 scale-50 transition-[opacity,scale] duration-300 delay-[350ms]">
<polyline points="20 6 9 17 4 12" pathLength="30" stroke-dasharray="30" stroke-dashoffset="30"></polyline>
</svg>
</button>
</div>
<p id="tab12-status" role="status" aria-live="polite" class="mt-3 text-center text-[0.8rem] text-tab12-mut min-h-5"></p>
</div>
</section>@import "tailwindcss";
@theme {
--color-tab12-bg: #f8fafc;
--color-tab12-bg: oklch(0.98 0.004 250);
--color-tab12-ink: #1e293b;
--color-tab12-ink: oklch(0.28 0.03 258);
--color-tab12-mut: #64748b;
--color-tab12-mut: oklch(0.55 0.03 255);
--color-tab12-edge: #e2e8f0;
--color-tab12-edge: oklch(0.92 0.01 250);
--color-tab12-primary: #4f46e5;
--color-tab12-primary: oklch(0.51 0.24 277);
--color-tab12-success: #10b981;
--color-tab12-success: oklch(0.7 0.15 163);
--font-tab12-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab12-draw: tab12-draw 0.4s ease 0.4s forwards;
}
@keyframes tab12-draw {
from {
stroke-dashoffset: 30;
}
to {
stroke-dashoffset: 0;
}
}
/* The two state-driven child rules that have no variant form worth writing:
the label fade and the checkmark reveal both key off the parent's data-state. */
.tab-12 [data-state="success"] > span {
opacity: 0;
scale: 0.6;
position: absolute;
}
.tab-12 [data-state="success"] > svg {
opacity: 1;
scale: 1;
}
.tab-12 [data-state="success"] polyline {
animation: var(--animate-tab12-draw);
}
.tab-12 {
width: 100%;
min-height: 100svh;
}
.tab-12 ::selection {
background: var(--color-tab12-success);
color: #04231a;
}
@media (prefers-reduced-motion: reduce) {
.tab-12 [data-state="success"] polyline {
animation: none;
stroke-dashoffset: 0;
}
}@import "tailwindcss";
@theme {
--color-tab12-bg: #f8fafc;
--color-tab12-bg: oklch(0.98 0.004 250);
--color-tab12-ink: #1e293b;
--color-tab12-ink: oklch(0.28 0.03 258);
--color-tab12-mut: #64748b;
--color-tab12-mut: oklch(0.55 0.03 255);
--color-tab12-edge: #e2e8f0;
--color-tab12-edge: oklch(0.92 0.01 250);
--color-tab12-primary: #4f46e5;
--color-tab12-primary: oklch(0.51 0.24 277);
--color-tab12-success: #10b981;
--color-tab12-success: oklch(0.7 0.15 163);
--font-tab12-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab12-draw: tab12-draw 0.4s ease 0.4s forwards;
}
@keyframes tab12-draw {
from {
stroke-dashoffset: 30;
}
to {
stroke-dashoffset: 0;
}
}
/* The two state-driven child rules that have no variant form worth writing:
the label fade and the checkmark reveal both key off the parent's data-state. */
.tab-12 [data-state="success"] > span {
opacity: 0;
scale: 0.6;
position: absolute;
}
.tab-12 [data-state="success"] > svg {
opacity: 1;
scale: 1;
}
.tab-12 [data-state="success"] polyline {
animation: var(--animate-tab12-draw);
}
.tab-12 {
width: 100%;
min-height: 100svh;
}
.tab-12 ::selection {
background: var(--color-tab12-success);
color: #04231a;
}
@media (prefers-reduced-motion: reduce) {
.tab-12 [data-state="success"] polyline {
animation: none;
stroke-dashoffset: 0;
}
}(function () {
var btn = document.getElementById('tab12-btn');
var status = document.getElementById('tab12-status');
if (!btn) return;
var timer = null;
btn.addEventListener('click', function () {
if (btn.dataset.state === 'success') return;
btn.dataset.state = 'success';
btn.disabled = true;
btn.setAttribute('aria-busy', 'false');
if (status) status.textContent = 'Message sent.';
clearTimeout(timer);
timer = setTimeout(function () {
btn.dataset.state = 'idle';
btn.disabled = false;
if (status) status.textContent = '';
}, 2500);
});
})();(function () {
var btn = document.getElementById('tab12-btn');
var status = document.getElementById('tab12-status');
if (!btn) return;
var timer = null;
btn.addEventListener('click', function () {
if (btn.dataset.state === 'success') return;
btn.dataset.state = 'success';
btn.disabled = true;
btn.setAttribute('aria-busy', 'false');
if (status) status.textContent = 'Message sent.';
clearTimeout(timer);
timer = setTimeout(function () {
btn.dataset.state = 'idle';
btn.disabled = false;
if (status) status.textContent = '';
}, 2500);
});
})();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-tab12-bg: #f8fafc;
--color-tab12-bg: oklch(0.98 0.004 250);
--color-tab12-ink: #1e293b;
--color-tab12-ink: oklch(0.28 0.03 258);
--color-tab12-mut: #64748b;
--color-tab12-mut: oklch(0.55 0.03 255);
--color-tab12-edge: #e2e8f0;
--color-tab12-edge: oklch(0.92 0.01 250);
--color-tab12-primary: #4f46e5;
--color-tab12-primary: oklch(0.51 0.24 277);
--color-tab12-success: #10b981;
--color-tab12-success: oklch(0.7 0.15 163);
--font-tab12-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab12-draw: tab12-draw 0.4s ease 0.4s forwards;
}
@keyframes tab12-draw {
from {
stroke-dashoffset: 30;
}
to {
stroke-dashoffset: 0;
}
}
/* The two state-driven child rules that have no variant form worth writing:
the label fade and the checkmark reveal both key off the parent's data-state. */
.tab-12 [data-state="success"] > span {
opacity: 0;
scale: 0.6;
position: absolute;
}
.tab-12 [data-state="success"] > svg {
opacity: 1;
scale: 1;
}
.tab-12 [data-state="success"] polyline {
animation: var(--animate-tab12-draw);
}
.tab-12 {
width: 100%;
min-height: 100svh;
}
.tab-12 ::selection {
background: var(--color-tab12-success);
color: #04231a;
}
@media (prefers-reduced-motion: reduce) {
.tab-12 [data-state="success"] polyline {
animation: none;
stroke-dashoffset: 0;
}
}@import "tailwindcss";
@theme {
--color-tab12-bg: #f8fafc;
--color-tab12-bg: oklch(0.98 0.004 250);
--color-tab12-ink: #1e293b;
--color-tab12-ink: oklch(0.28 0.03 258);
--color-tab12-mut: #64748b;
--color-tab12-mut: oklch(0.55 0.03 255);
--color-tab12-edge: #e2e8f0;
--color-tab12-edge: oklch(0.92 0.01 250);
--color-tab12-primary: #4f46e5;
--color-tab12-primary: oklch(0.51 0.24 277);
--color-tab12-success: #10b981;
--color-tab12-success: oklch(0.7 0.15 163);
--font-tab12-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab12-draw: tab12-draw 0.4s ease 0.4s forwards;
}
@keyframes tab12-draw {
from {
stroke-dashoffset: 30;
}
to {
stroke-dashoffset: 0;
}
}
/* The two state-driven child rules that have no variant form worth writing:
the label fade and the checkmark reveal both key off the parent's data-state. */
.tab-12 [data-state="success"] > span {
opacity: 0;
scale: 0.6;
position: absolute;
}
.tab-12 [data-state="success"] > svg {
opacity: 1;
scale: 1;
}
.tab-12 [data-state="success"] polyline {
animation: var(--animate-tab12-draw);
}
.tab-12 {
width: 100%;
min-height: 100svh;
}
.tab-12 ::selection {
background: var(--color-tab12-success);
color: #04231a;
}
@media (prefers-reduced-motion: reduce) {
.tab-12 [data-state="success"] polyline {
animation: none;
stroke-dashoffset: 0;
}
}<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.
- Two class toggles become one
data-statevalue, and the three child transitions that read it stay in CSS — a parent-attribute-to-child selector has a variant form (data-[state=success]:[&_polyline]:) but it is less legible than the three-line rule, so this is one of the places the port deliberately keeps CSS. - The morph is
widthonly. Radius was already 9999px, so the pill becomes a circle for free — and the Safari rectangle flash the CSS version warned about cannot happen. - UI change from the CSS original: an
aria-livestatus line. A checkmark drawing itself is invisible to a screen reader; "Message sent." is not.
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 Submit Button Success State with Checkmark Draw
Source: https://codefronts.com/motion/tailwind-css-animated-buttons/tailwind-submit-button-success-state/
A submit button that collapses into a circle and draws a checkmark. The port replaces a class toggle with data-state, so the markup reads data-[state=success]:w-12 and the impossible state ("success and idle at once") stops existing.
## HTML
```html
<section class="tab-12 w-full min-h-svh grid place-items-center bg-tab12-bg font-tab12-sans px-6 py-10">
<div class="w-[min(380px,100%)] p-8 bg-white rounded-2xl shadow-[0_4px_24px_oklch(0_0_0/0.08)]">
<h2 class="text-[1.4rem] font-bold text-tab12-ink m-0">Contact Us</h2>
<p class="mt-1.5 mb-6 text-[0.88rem] leading-relaxed text-tab12-mut">We'll get back to you within 24 hours.</p>
<label class="block mb-3.5">
<span class="sr-only">Your name</span>
<input type="text" placeholder="Your name" class="w-full min-h-11 px-4 text-[0.9rem] text-tab12-ink bg-tab12-bg border-[1.5px] border-tab12-edge rounded-lg transition-colors duration-200 focus:border-tab12-primary focus:outline-2 focus:outline-offset-2 focus:outline-tab12-primary/40">
</label>
<label class="block mb-4">
<span class="sr-only">Email address</span>
<input type="email" placeholder="Email address" class="w-full min-h-11 px-4 text-[0.9rem] text-tab12-ink bg-tab12-bg border-[1.5px] border-tab12-edge rounded-lg transition-colors duration-200 focus:border-tab12-primary focus:outline-2 focus:outline-offset-2 focus:outline-tab12-primary/40">
</label>
<div class="flex justify-center">
<button type="button" id="tab12-btn" data-state="idle" class="relative flex items-center justify-center w-full h-12 px-6 text-base font-bold text-white bg-tab12-primary rounded-full cursor-pointer overflow-hidden shadow-[0_4px_16px_oklch(0.51_0.24_277/0.3)] transition-[width,background-color,box-shadow] duration-[400ms] ease-[cubic-bezier(0.4,0,0.2,1)] hover:brightness-108 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab12-primary data-[state=success]:w-12 data-[state=success]:bg-tab12-success data-[state=success]:shadow-[0_4px_16px_oklch(0.7_0.15_163/0.35)] data-[state=success]:cursor-default motion-reduce:transition-none">
<span class="whitespace-nowrap transition-[opacity,scale] duration-200">Send Message</span>
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="absolute size-6 opacity-0 scale-50 transition-[opacity,scale] duration-300 delay-[350ms]">
<polyline points="20 6 9 17 4 12" pathLength="30" stroke-dasharray="30" stroke-dashoffset="30"></polyline>
</svg>
</button>
</div>
<p id="tab12-status" role="status" aria-live="polite" class="mt-3 text-center text-[0.8rem] text-tab12-mut min-h-5"></p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tab12-bg: #f8fafc;
--color-tab12-bg: oklch(0.98 0.004 250);
--color-tab12-ink: #1e293b;
--color-tab12-ink: oklch(0.28 0.03 258);
--color-tab12-mut: #64748b;
--color-tab12-mut: oklch(0.55 0.03 255);
--color-tab12-edge: #e2e8f0;
--color-tab12-edge: oklch(0.92 0.01 250);
--color-tab12-primary: #4f46e5;
--color-tab12-primary: oklch(0.51 0.24 277);
--color-tab12-success: #10b981;
--color-tab12-success: oklch(0.7 0.15 163);
--font-tab12-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab12-draw: tab12-draw 0.4s ease 0.4s forwards;
}
@keyframes tab12-draw {
from {
stroke-dashoffset: 30;
}
to {
stroke-dashoffset: 0;
}
}
/* The two state-driven child rules that have no variant form worth writing:
the label fade and the checkmark reveal both key off the parent's data-state. */
.tab-12 [data-state="success"] > span {
opacity: 0;
scale: 0.6;
position: absolute;
}
.tab-12 [data-state="success"] > svg {
opacity: 1;
scale: 1;
}
.tab-12 [data-state="success"] polyline {
animation: var(--animate-tab12-draw);
}
.tab-12 {
width: 100%;
min-height: 100svh;
}
.tab-12 ::selection {
background: var(--color-tab12-success);
color: #04231a;
}
@media (prefers-reduced-motion: reduce) {
.tab-12 [data-state="success"] polyline {
animation: none;
stroke-dashoffset: 0;
}
}
```
## JavaScript
```js
(function () {
var btn = document.getElementById('tab12-btn');
var status = document.getElementById('tab12-status');
if (!btn) return;
var timer = null;
btn.addEventListener('click', function () {
if (btn.dataset.state === 'success') return;
btn.dataset.state = 'success';
btn.disabled = true;
btn.setAttribute('aria-busy', 'false');
if (status) status.textContent = 'Message sent.';
clearTimeout(timer);
timer = setTimeout(function () {
btn.dataset.state = 'idle';
btn.disabled = false;
if (status) status.textContent = '';
}, 2500);
});
})();
```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 Submit Button Success State with Checkmark Draw
Source: https://codefronts.com/motion/tailwind-css-animated-buttons/tailwind-submit-button-success-state/
A submit button that collapses into a circle and draws a checkmark. The port replaces a class toggle with data-state, so the markup reads data-[state=success]:w-12 and the impossible state ("success and idle at once") stops existing.
## HTML
```html
<section class="tab-12 w-full min-h-svh grid place-items-center bg-tab12-bg font-tab12-sans px-6 py-10">
<div class="w-[min(380px,100%)] p-8 bg-white rounded-2xl shadow-[0_4px_24px_oklch(0_0_0/0.08)]">
<h2 class="text-[1.4rem] font-bold text-tab12-ink m-0">Contact Us</h2>
<p class="mt-1.5 mb-6 text-[0.88rem] leading-relaxed text-tab12-mut">We'll get back to you within 24 hours.</p>
<label class="block mb-3.5">
<span class="sr-only">Your name</span>
<input type="text" placeholder="Your name" class="w-full min-h-11 px-4 text-[0.9rem] text-tab12-ink bg-tab12-bg border-[1.5px] border-tab12-edge rounded-lg transition-colors duration-200 focus:border-tab12-primary focus:outline-2 focus:outline-offset-2 focus:outline-tab12-primary/40">
</label>
<label class="block mb-4">
<span class="sr-only">Email address</span>
<input type="email" placeholder="Email address" class="w-full min-h-11 px-4 text-[0.9rem] text-tab12-ink bg-tab12-bg border-[1.5px] border-tab12-edge rounded-lg transition-colors duration-200 focus:border-tab12-primary focus:outline-2 focus:outline-offset-2 focus:outline-tab12-primary/40">
</label>
<div class="flex justify-center">
<button type="button" id="tab12-btn" data-state="idle" class="relative flex items-center justify-center w-full h-12 px-6 text-base font-bold text-white bg-tab12-primary rounded-full cursor-pointer overflow-hidden shadow-[0_4px_16px_oklch(0.51_0.24_277/0.3)] transition-[width,background-color,box-shadow] duration-[400ms] ease-[cubic-bezier(0.4,0,0.2,1)] hover:brightness-108 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab12-primary data-[state=success]:w-12 data-[state=success]:bg-tab12-success data-[state=success]:shadow-[0_4px_16px_oklch(0.7_0.15_163/0.35)] data-[state=success]:cursor-default motion-reduce:transition-none">
<span class="whitespace-nowrap transition-[opacity,scale] duration-200">Send Message</span>
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="absolute size-6 opacity-0 scale-50 transition-[opacity,scale] duration-300 delay-[350ms]">
<polyline points="20 6 9 17 4 12" pathLength="30" stroke-dasharray="30" stroke-dashoffset="30"></polyline>
</svg>
</button>
</div>
<p id="tab12-status" role="status" aria-live="polite" class="mt-3 text-center text-[0.8rem] text-tab12-mut min-h-5"></p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tab12-bg: #f8fafc;
--color-tab12-bg: oklch(0.98 0.004 250);
--color-tab12-ink: #1e293b;
--color-tab12-ink: oklch(0.28 0.03 258);
--color-tab12-mut: #64748b;
--color-tab12-mut: oklch(0.55 0.03 255);
--color-tab12-edge: #e2e8f0;
--color-tab12-edge: oklch(0.92 0.01 250);
--color-tab12-primary: #4f46e5;
--color-tab12-primary: oklch(0.51 0.24 277);
--color-tab12-success: #10b981;
--color-tab12-success: oklch(0.7 0.15 163);
--font-tab12-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tab12-draw: tab12-draw 0.4s ease 0.4s forwards;
}
@keyframes tab12-draw {
from {
stroke-dashoffset: 30;
}
to {
stroke-dashoffset: 0;
}
}
/* The two state-driven child rules that have no variant form worth writing:
the label fade and the checkmark reveal both key off the parent's data-state. */
.tab-12 [data-state="success"] > span {
opacity: 0;
scale: 0.6;
position: absolute;
}
.tab-12 [data-state="success"] > svg {
opacity: 1;
scale: 1;
}
.tab-12 [data-state="success"] polyline {
animation: var(--animate-tab12-draw);
}
.tab-12 {
width: 100%;
min-height: 100svh;
}
.tab-12 ::selection {
background: var(--color-tab12-success);
color: #04231a;
}
@media (prefers-reduced-motion: reduce) {
.tab-12 [data-state="success"] polyline {
animation: none;
stroke-dashoffset: 0;
}
}
```
## JavaScript
```js
(function () {
var btn = document.getElementById('tab12-btn');
var status = document.getElementById('tab12-status');
if (!btn) return;
var timer = null;
btn.addEventListener('click', function () {
if (btn.dataset.state === 'success') return;
btn.dataset.state = 'success';
btn.disabled = true;
btn.setAttribute('aria-busy', 'false');
if (status) status.textContent = 'Message sent.';
clearTimeout(timer);
timer = setTimeout(function () {
btn.dataset.state = 'idle';
btn.disabled = false;
if (status) status.textContent = '';
}, 2500);
});
})();
```How this works
One attribute, not three booleans. The CSS version toggled .is-success; a sibling of that pattern (demo 13) toggled two classes that could both be present. Here the controller writes data-state="idle" | "success" and every visual reads it with data-[state=success]:. The state machine is now representable in the DOM exactly once.
The checkmark draws from a dash offset. The polyline carries pathLength="30" with stroke-dasharray:30, so the keyframe simply walks stroke-dashoffset to 0. The child selector [&_polyline]: is how a utility reaches into SVG without giving the path its own class.
The collapse is width-only. The button sits in a fixed-width flex column, so shrinking from full width to 48px shifts nothing around it — the layout cost the original warned about is designed out rather than documented.
Make it yours
- Success colour:
--color-tab12-successdrives fill, shadow and the ring. - Hold time: the
2500in the controller'ssetTimeout. - Add an error branch by writing
data-state="error"and adata-[state=error]:set — no new boolean, no conflict.
Gotchas — read before shipping
- Give the keyframe explicit
fromandtooffsets. Ato-only keyframe takes its start from the computed value, and when that value comes from a parent's[&_polyline]:descendant utility the tick can resolve to a permanent 30px offset — an invisible checkmark with the animation apparently running. - Change the checkmark path and you must change
pathLengthwith it, or the dash maths no longer reaches zero. - Keep the button in a container with a resolved width. A collapse from
100%to48pxinside an auto-width parent reflows its siblings. - Disable the button while the success state shows — otherwise a fast second click starts a second submit while the first is still confirming.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
SVG dash animation and width transitions are universally supported; the floor is Tailwind v4's generated sheet.