03 / 28
All 28 in 28 Tailwind Hamburger Menus
- 01Tailwind Hamburger Menu with Clip-Path Circle Expansion
- 02Tailwind Keyboard Navigable Hamburger Menu with ARIA Attributes
- 03Tailwind Animated 3-Line to X Hamburger Icon Toggle
- 04Tailwind Slide-In Off-Canvas Drawer with Dimmed Backdrop Overlay
- 05Pure Tailwind Checkbox Hack Hamburger Navigation (No JavaScript)
- 06Tailwind Responsive Hamburger Menu with Integrated Search Bar & Social Links
- 07Tailwind Glassmorphism Backdrop Blur Hamburger Navigation
- 08Tailwind Push Page Navigation Menu (Off-Canvas Content Shift)
- 09Pure Tailwind Hamburger Menu Without JavaScript
- 10Tailwind Hamburger Menu Morphing to Close Icon Animation
- 11Tailwind Full Screen Mobile Navigation Overlay
- 12Tailwind Slide Out Sidebar Navigation / Off Canvas Menu
- 13Tailwind Responsive Navbar Mobile Hamburger Toggle Breakpoints
- 14Tailwind Minimalist Floating Hamburger Button
- 15Tailwind CSS Responsive Hamburger Menu Component
- 16Tailwind Rebuild of the Bootstrap 5 Navbar Hamburger Toggle
- 17Tailwind WCAG Accessible Hamburger Menu Example
- 18Pure Tailwind Dropdown Slide Down Mobile Hamburger Menu
- 19Simple 3-Line Tailwind Hamburger Icon Hover Effect
- 20Tailwind Full Screen Blur Overlay Hamburger Menu
- 21Tailwind Mobile Bottom Navigation Bar with Floating Hamburger Toggle
- 22Pure Tailwind Multi-Level Accordion Hamburger Dropdown Menu
- 23Flexbox Navbar Right-Aligned Tailwind Hamburger Icon
- 24Radial Ripple Expand Tailwind Hamburger Menu
- 25Sticky Tailwind Navbar Shrinking Hamburger Icon on Scroll
- 26Neumorphic Tailwind Hamburger Toggle Button
- 27Brutalist Design Bordered Tailwind Hamburger Menu
- 28Tailwind 3D Flip Canvas Mobile Hamburger Navigation
Tailwind Animated 3-Line to X Hamburger Icon Toggle
Four production-grade line-to-X morphs on one stage — Spin, Squeeze, Arrow and Drop — and the port deletes the variant classes entirely. There is no .spin or .squeeze rule: each tile is a group whose three bars carry their own end states as group-aria-pressed: utilities, so a variant is now a class list you copy rather than a stylesheet block you maintain.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="thb-03 w-full min-h-svh bg-thb03-bg font-thb03-sans text-thb03-ink px-5 py-9" aria-label="Animated hamburger to X icon variants demo">
<div class="p-6 rounded-[20px] border border-thb03-edge bg-thb03-bg bg-[radial-gradient(100%_90%_at_50%_-20%,oklch(0.26_0.05_285/0.7),transparent_60%)]">
<header class="flex items-baseline justify-between gap-3 flex-wrap mb-5">
<h2 class="text-base font-semibold tracking-[-0.01em]">Line → X morphs</h2>
<span class="text-[11.5px] text-thb03-mut">click a tile · real aria-pressed buttons</span>
</header>
<div class="grid grid-cols-[repeat(auto-fit,minmax(min(150px,100%),1fr))] gap-3.5">
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle spin morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4 transition-[rotate] duration-450 ease-thb03out group-aria-pressed:rotate-180 motion-reduce:transition-none" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 ease-thb03 group-aria-pressed:translate-y-[7px] group-aria-pressed:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current transition-[opacity,scale] duration-250 ease-thb03 group-aria-pressed:opacity-0 group-aria-pressed:scale-x-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 ease-thb03 group-aria-pressed:-translate-y-[7px] group-aria-pressed:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Spin</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">rotate 180° · fold 45°</code></figcaption>
</figure>
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle squeeze morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.2s_cubic-bezier(.55,0,.55,1),rotate_.25s_var(--ease-thb03)_.18s,opacity_.15s_.08s] group-aria-pressed:translate-y-[7px] group-aria-pressed:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current [transition:opacity_.15s_.08s] group-aria-pressed:opacity-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.2s_cubic-bezier(.55,0,.55,1),rotate_.25s_var(--ease-thb03)_.18s,opacity_.15s_.08s] group-aria-pressed:-translate-y-[7px] group-aria-pressed:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Squeeze</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">converge → rotate, 2 beats</code></figcaption>
</figure>
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle arrow morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current origin-left transition-[translate,rotate,scale] duration-300 ease-thb03 group-aria-pressed:scale-x-[0.62] group-aria-pressed:-rotate-40 group-aria-pressed:translate-y-px motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current origin-left transition-[scale] duration-300 ease-thb03 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current origin-left transition-[translate,rotate,scale] duration-300 ease-thb03 group-aria-pressed:scale-x-[0.62] group-aria-pressed:rotate-40 group-aria-pressed:-translate-y-px motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Arrow</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">scale-x .62 · origin-left</code></figcaption>
</figure>
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle drop morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.32s_cubic-bezier(.5,0,.9,.6),rotate_.3s_var(--ease-thb03)_.16s,opacity_.26s_ease-in] group-aria-pressed:translate-y-[7px] group-aria-pressed:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.32s_cubic-bezier(.5,0,.9,.6),rotate_.3s_var(--ease-thb03)_.16s,opacity_.26s_ease-in] group-aria-pressed:translate-x-[3px] group-aria-pressed:translate-y-4 group-aria-pressed:rotate-[14deg] group-aria-pressed:opacity-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.32s_cubic-bezier(.5,0,.9,.6),rotate_.3s_var(--ease-thb03)_.16s,opacity_.26s_ease-in] group-aria-pressed:-translate-y-[7px] group-aria-pressed:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Drop</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">mid falls · outers cross late</code></figcaption>
</figure>
</div>
</div>
</section><section class="thb-03 w-full min-h-svh bg-thb03-bg font-thb03-sans text-thb03-ink px-5 py-9" aria-label="Animated hamburger to X icon variants demo">
<div class="p-6 rounded-[20px] border border-thb03-edge bg-thb03-bg bg-[radial-gradient(100%_90%_at_50%_-20%,oklch(0.26_0.05_285/0.7),transparent_60%)]">
<header class="flex items-baseline justify-between gap-3 flex-wrap mb-5">
<h2 class="text-base font-semibold tracking-[-0.01em]">Line → X morphs</h2>
<span class="text-[11.5px] text-thb03-mut">click a tile · real aria-pressed buttons</span>
</header>
<div class="grid grid-cols-[repeat(auto-fit,minmax(min(150px,100%),1fr))] gap-3.5">
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle spin morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4 transition-[rotate] duration-450 ease-thb03out group-aria-pressed:rotate-180 motion-reduce:transition-none" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 ease-thb03 group-aria-pressed:translate-y-[7px] group-aria-pressed:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current transition-[opacity,scale] duration-250 ease-thb03 group-aria-pressed:opacity-0 group-aria-pressed:scale-x-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 ease-thb03 group-aria-pressed:-translate-y-[7px] group-aria-pressed:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Spin</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">rotate 180° · fold 45°</code></figcaption>
</figure>
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle squeeze morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.2s_cubic-bezier(.55,0,.55,1),rotate_.25s_var(--ease-thb03)_.18s,opacity_.15s_.08s] group-aria-pressed:translate-y-[7px] group-aria-pressed:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current [transition:opacity_.15s_.08s] group-aria-pressed:opacity-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.2s_cubic-bezier(.55,0,.55,1),rotate_.25s_var(--ease-thb03)_.18s,opacity_.15s_.08s] group-aria-pressed:-translate-y-[7px] group-aria-pressed:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Squeeze</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">converge → rotate, 2 beats</code></figcaption>
</figure>
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle arrow morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current origin-left transition-[translate,rotate,scale] duration-300 ease-thb03 group-aria-pressed:scale-x-[0.62] group-aria-pressed:-rotate-40 group-aria-pressed:translate-y-px motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current origin-left transition-[scale] duration-300 ease-thb03 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current origin-left transition-[translate,rotate,scale] duration-300 ease-thb03 group-aria-pressed:scale-x-[0.62] group-aria-pressed:rotate-40 group-aria-pressed:-translate-y-px motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Arrow</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">scale-x .62 · origin-left</code></figcaption>
</figure>
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle drop morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.32s_cubic-bezier(.5,0,.9,.6),rotate_.3s_var(--ease-thb03)_.16s,opacity_.26s_ease-in] group-aria-pressed:translate-y-[7px] group-aria-pressed:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.32s_cubic-bezier(.5,0,.9,.6),rotate_.3s_var(--ease-thb03)_.16s,opacity_.26s_ease-in] group-aria-pressed:translate-x-[3px] group-aria-pressed:translate-y-4 group-aria-pressed:rotate-[14deg] group-aria-pressed:opacity-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.32s_cubic-bezier(.5,0,.9,.6),rotate_.3s_var(--ease-thb03)_.16s,opacity_.26s_ease-in] group-aria-pressed:-translate-y-[7px] group-aria-pressed:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Drop</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">mid falls · outers cross late</code></figcaption>
</figure>
</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-thb03-bg: #23252f;
--color-thb03-bg: oklch(0.17 0.02 275);
--color-thb03-card: #2f3140;
--color-thb03-card: oklch(0.22 0.025 275);
--color-thb03-edge: #454858;
--color-thb03-edge: oklch(0.3 0.03 275);
--color-thb03-ink: #f0f0f4;
--color-thb03-ink: oklch(0.95 0.008 275);
--color-thb03-mut: #9a9daa;
--color-thb03-mut: oklch(0.65 0.02 275);
--color-thb03-acc: #57d3a4;
--color-thb03-acc: oklch(0.75 0.15 165);
--font-thb03-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb03-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb03: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-thb03out: cubic-bezier(0.3, 0.9, 0.35, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.thb-03 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-thb03-bg: #23252f;
--color-thb03-bg: oklch(0.17 0.02 275);
--color-thb03-card: #2f3140;
--color-thb03-card: oklch(0.22 0.025 275);
--color-thb03-edge: #454858;
--color-thb03-edge: oklch(0.3 0.03 275);
--color-thb03-ink: #f0f0f4;
--color-thb03-ink: oklch(0.95 0.008 275);
--color-thb03-mut: #9a9daa;
--color-thb03-mut: oklch(0.65 0.02 275);
--color-thb03-acc: #57d3a4;
--color-thb03-acc: oklch(0.75 0.15 165);
--font-thb03-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb03-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb03: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-thb03out: cubic-bezier(0.3, 0.9, 0.35, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.thb-03 {
width: 100%;
min-height: 100svh;
}(function () {
document.querySelectorAll('.thb-03 button[aria-pressed]').forEach(function (btn) {
if (btn.dataset.bound) return; btn.dataset.bound = '1';
btn.addEventListener('click', function () {
btn.setAttribute('aria-pressed', String(btn.getAttribute('aria-pressed') !== 'true'));
});
});
})();(function () {
document.querySelectorAll('.thb-03 button[aria-pressed]').forEach(function (btn) {
if (btn.dataset.bound) return; btn.dataset.bound = '1';
btn.addEventListener('click', function () {
btn.setAttribute('aria-pressed', String(btn.getAttribute('aria-pressed') !== 'true'));
});
});
})();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-thb03-bg: #23252f;
--color-thb03-bg: oklch(0.17 0.02 275);
--color-thb03-card: #2f3140;
--color-thb03-card: oklch(0.22 0.025 275);
--color-thb03-edge: #454858;
--color-thb03-edge: oklch(0.3 0.03 275);
--color-thb03-ink: #f0f0f4;
--color-thb03-ink: oklch(0.95 0.008 275);
--color-thb03-mut: #9a9daa;
--color-thb03-mut: oklch(0.65 0.02 275);
--color-thb03-acc: #57d3a4;
--color-thb03-acc: oklch(0.75 0.15 165);
--font-thb03-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb03-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb03: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-thb03out: cubic-bezier(0.3, 0.9, 0.35, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.thb-03 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-thb03-bg: #23252f;
--color-thb03-bg: oklch(0.17 0.02 275);
--color-thb03-card: #2f3140;
--color-thb03-card: oklch(0.22 0.025 275);
--color-thb03-edge: #454858;
--color-thb03-edge: oklch(0.3 0.03 275);
--color-thb03-ink: #f0f0f4;
--color-thb03-ink: oklch(0.95 0.008 275);
--color-thb03-mut: #9a9daa;
--color-thb03-mut: oklch(0.65 0.02 275);
--color-thb03-acc: #57d3a4;
--color-thb03-acc: oklch(0.75 0.15 165);
--font-thb03-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb03-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb03: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-thb03out: cubic-bezier(0.3, 0.9, 0.35, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.thb-03 {
width: 100%;
min-height: 100svh;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The four variant classes disappear. There is no
.spin/.squeeze/.arrow/.droprule anywhere — each tile's bars carry their owngroup-aria-pressed:end states, so shipping a variant means copying one element rather than porting a stylesheet block. [aria-pressed="true"] .xbecomesgroup-aria-pressed:on descendants andaria-pressed:on the button itself — the toggle's accessibility state is the only state in the demo.- Tailwind's
duration-*/delay-*apply to every transitioned property at once, so the two multi-beat variants (Squeeze, Drop) keep an arbitrary[transition:…]shorthand. Spin and Arrow are single-beat and stay on the utility scale. scale-x-0andscale-x-[0.62]replace thescale:0 1/scale:.62 1pairs, andorigin-leftreplaces thetransform-originrule.- The
auto-fittile grid needs no media query and no container query — it is one arbitrarygrid-cols-[…]value on the wrapper. - The
@media (prefers-reduced-motion)blanket rule becomesmotion-reduce:transition-nonerepeated per bar. More verbose, but each element states its own fallback.
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 3-Line to X Hamburger Icon Toggle
Source: https://codefronts.com/navigation/tailwind-hamburger-menus/tailwind-animated-3-line-to-x-hamburger-icon-toggle/
Four production-grade line-to-X morphs on one stage — Spin, Squeeze, Arrow and Drop — and the port deletes the variant classes entirely. There is no .spin or .squeeze rule: each tile is a group whose three bars carry their own end states as group-aria-pressed: utilities, so a variant is now a class list you copy rather than a stylesheet block you maintain.
## HTML
```html
<section class="thb-03 w-full min-h-svh bg-thb03-bg font-thb03-sans text-thb03-ink px-5 py-9" aria-label="Animated hamburger to X icon variants demo">
<div class="p-6 rounded-[20px] border border-thb03-edge bg-thb03-bg bg-[radial-gradient(100%_90%_at_50%_-20%,oklch(0.26_0.05_285/0.7),transparent_60%)]">
<header class="flex items-baseline justify-between gap-3 flex-wrap mb-5">
<h2 class="text-base font-semibold tracking-[-0.01em]">Line → X morphs</h2>
<span class="text-[11.5px] text-thb03-mut">click a tile · real aria-pressed buttons</span>
</header>
<div class="grid grid-cols-[repeat(auto-fit,minmax(min(150px,100%),1fr))] gap-3.5">
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle spin morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4 transition-[rotate] duration-450 ease-thb03out group-aria-pressed:rotate-180 motion-reduce:transition-none" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 ease-thb03 group-aria-pressed:translate-y-[7px] group-aria-pressed:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current transition-[opacity,scale] duration-250 ease-thb03 group-aria-pressed:opacity-0 group-aria-pressed:scale-x-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 ease-thb03 group-aria-pressed:-translate-y-[7px] group-aria-pressed:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Spin</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">rotate 180° · fold 45°</code></figcaption>
</figure>
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle squeeze morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.2s_cubic-bezier(.55,0,.55,1),rotate_.25s_var(--ease-thb03)_.18s,opacity_.15s_.08s] group-aria-pressed:translate-y-[7px] group-aria-pressed:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current [transition:opacity_.15s_.08s] group-aria-pressed:opacity-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.2s_cubic-bezier(.55,0,.55,1),rotate_.25s_var(--ease-thb03)_.18s,opacity_.15s_.08s] group-aria-pressed:-translate-y-[7px] group-aria-pressed:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Squeeze</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">converge → rotate, 2 beats</code></figcaption>
</figure>
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle arrow morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current origin-left transition-[translate,rotate,scale] duration-300 ease-thb03 group-aria-pressed:scale-x-[0.62] group-aria-pressed:-rotate-40 group-aria-pressed:translate-y-px motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current origin-left transition-[scale] duration-300 ease-thb03 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current origin-left transition-[translate,rotate,scale] duration-300 ease-thb03 group-aria-pressed:scale-x-[0.62] group-aria-pressed:rotate-40 group-aria-pressed:-translate-y-px motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Arrow</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">scale-x .62 · origin-left</code></figcaption>
</figure>
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle drop morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.32s_cubic-bezier(.5,0,.9,.6),rotate_.3s_var(--ease-thb03)_.16s,opacity_.26s_ease-in] group-aria-pressed:translate-y-[7px] group-aria-pressed:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.32s_cubic-bezier(.5,0,.9,.6),rotate_.3s_var(--ease-thb03)_.16s,opacity_.26s_ease-in] group-aria-pressed:translate-x-[3px] group-aria-pressed:translate-y-4 group-aria-pressed:rotate-[14deg] group-aria-pressed:opacity-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.32s_cubic-bezier(.5,0,.9,.6),rotate_.3s_var(--ease-thb03)_.16s,opacity_.26s_ease-in] group-aria-pressed:-translate-y-[7px] group-aria-pressed:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Drop</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">mid falls · outers cross late</code></figcaption>
</figure>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-thb03-bg: #23252f;
--color-thb03-bg: oklch(0.17 0.02 275);
--color-thb03-card: #2f3140;
--color-thb03-card: oklch(0.22 0.025 275);
--color-thb03-edge: #454858;
--color-thb03-edge: oklch(0.3 0.03 275);
--color-thb03-ink: #f0f0f4;
--color-thb03-ink: oklch(0.95 0.008 275);
--color-thb03-mut: #9a9daa;
--color-thb03-mut: oklch(0.65 0.02 275);
--color-thb03-acc: #57d3a4;
--color-thb03-acc: oklch(0.75 0.15 165);
--font-thb03-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb03-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb03: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-thb03out: cubic-bezier(0.3, 0.9, 0.35, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.thb-03 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(function () {
document.querySelectorAll('.thb-03 button[aria-pressed]').forEach(function (btn) {
if (btn.dataset.bound) return; btn.dataset.bound = '1';
btn.addEventListener('click', function () {
btn.setAttribute('aria-pressed', String(btn.getAttribute('aria-pressed') !== 'true'));
});
});
})();
```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 3-Line to X Hamburger Icon Toggle
Source: https://codefronts.com/navigation/tailwind-hamburger-menus/tailwind-animated-3-line-to-x-hamburger-icon-toggle/
Four production-grade line-to-X morphs on one stage — Spin, Squeeze, Arrow and Drop — and the port deletes the variant classes entirely. There is no .spin or .squeeze rule: each tile is a group whose three bars carry their own end states as group-aria-pressed: utilities, so a variant is now a class list you copy rather than a stylesheet block you maintain.
## HTML
```html
<section class="thb-03 w-full min-h-svh bg-thb03-bg font-thb03-sans text-thb03-ink px-5 py-9" aria-label="Animated hamburger to X icon variants demo">
<div class="p-6 rounded-[20px] border border-thb03-edge bg-thb03-bg bg-[radial-gradient(100%_90%_at_50%_-20%,oklch(0.26_0.05_285/0.7),transparent_60%)]">
<header class="flex items-baseline justify-between gap-3 flex-wrap mb-5">
<h2 class="text-base font-semibold tracking-[-0.01em]">Line → X morphs</h2>
<span class="text-[11.5px] text-thb03-mut">click a tile · real aria-pressed buttons</span>
</header>
<div class="grid grid-cols-[repeat(auto-fit,minmax(min(150px,100%),1fr))] gap-3.5">
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle spin morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4 transition-[rotate] duration-450 ease-thb03out group-aria-pressed:rotate-180 motion-reduce:transition-none" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 ease-thb03 group-aria-pressed:translate-y-[7px] group-aria-pressed:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current transition-[opacity,scale] duration-250 ease-thb03 group-aria-pressed:opacity-0 group-aria-pressed:scale-x-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 ease-thb03 group-aria-pressed:-translate-y-[7px] group-aria-pressed:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Spin</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">rotate 180° · fold 45°</code></figcaption>
</figure>
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle squeeze morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.2s_cubic-bezier(.55,0,.55,1),rotate_.25s_var(--ease-thb03)_.18s,opacity_.15s_.08s] group-aria-pressed:translate-y-[7px] group-aria-pressed:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current [transition:opacity_.15s_.08s] group-aria-pressed:opacity-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.2s_cubic-bezier(.55,0,.55,1),rotate_.25s_var(--ease-thb03)_.18s,opacity_.15s_.08s] group-aria-pressed:-translate-y-[7px] group-aria-pressed:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Squeeze</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">converge → rotate, 2 beats</code></figcaption>
</figure>
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle arrow morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current origin-left transition-[translate,rotate,scale] duration-300 ease-thb03 group-aria-pressed:scale-x-[0.62] group-aria-pressed:-rotate-40 group-aria-pressed:translate-y-px motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current origin-left transition-[scale] duration-300 ease-thb03 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current origin-left transition-[translate,rotate,scale] duration-300 ease-thb03 group-aria-pressed:scale-x-[0.62] group-aria-pressed:rotate-40 group-aria-pressed:-translate-y-px motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Arrow</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">scale-x .62 · origin-left</code></figcaption>
</figure>
<figure class="flex flex-col items-center gap-3">
<button type="button" aria-pressed="false" aria-label="Toggle drop morph demo" class="group w-full aspect-[1.25] grid place-items-center rounded-2xl border border-thb03-edge bg-linear-[165deg,color-mix(in_oklch,var(--color-thb03-card)_88%,white),var(--color-thb03-card)_60%] text-thb03-ink cursor-pointer transition-[border-color,translate] duration-250 hover:-translate-y-0.5 hover:border-[color-mix(in_oklch,var(--color-thb03-acc)_55%,var(--color-thb03-edge))] focus-visible:outline-2 focus-visible:outline-thb03-acc focus-visible:outline-offset-3 aria-pressed:border-thb03-acc aria-pressed:text-thb03-acc motion-reduce:transition-none">
<span class="relative block w-5.5 h-4" aria-hidden="true">
<i class="absolute left-0 top-0 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.32s_cubic-bezier(.5,0,.9,.6),rotate_.3s_var(--ease-thb03)_.16s,opacity_.26s_ease-in] group-aria-pressed:translate-y-[7px] group-aria-pressed:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[7px] w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.32s_cubic-bezier(.5,0,.9,.6),rotate_.3s_var(--ease-thb03)_.16s,opacity_.26s_ease-in] group-aria-pressed:translate-x-[3px] group-aria-pressed:translate-y-4 group-aria-pressed:rotate-[14deg] group-aria-pressed:opacity-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-3.5 w-5.5 h-0.5 rounded-sm bg-current [transition:translate_.32s_cubic-bezier(.5,0,.9,.6),rotate_.3s_var(--ease-thb03)_.16s,opacity_.26s_ease-in] group-aria-pressed:-translate-y-[7px] group-aria-pressed:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
<figcaption class="grid justify-items-center gap-1 text-center"><b class="text-[13px] font-semibold">Drop</b><code class="font-thb03-mono text-[10.5px] text-thb03-mut">mid falls · outers cross late</code></figcaption>
</figure>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-thb03-bg: #23252f;
--color-thb03-bg: oklch(0.17 0.02 275);
--color-thb03-card: #2f3140;
--color-thb03-card: oklch(0.22 0.025 275);
--color-thb03-edge: #454858;
--color-thb03-edge: oklch(0.3 0.03 275);
--color-thb03-ink: #f0f0f4;
--color-thb03-ink: oklch(0.95 0.008 275);
--color-thb03-mut: #9a9daa;
--color-thb03-mut: oklch(0.65 0.02 275);
--color-thb03-acc: #57d3a4;
--color-thb03-acc: oklch(0.75 0.15 165);
--font-thb03-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb03-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb03: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-thb03out: cubic-bezier(0.3, 0.9, 0.35, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.thb-03 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(function () {
document.querySelectorAll('.thb-03 button[aria-pressed]').forEach(function (btn) {
if (btn.dataset.bound) return; btn.dataset.bound = '1';
btn.addEventListener('click', function () {
btn.setAttribute('aria-pressed', String(btn.getAttribute('aria-pressed') !== 'true'));
});
});
})();
```How this works
The variant lives on the element now. All four morphs share one skeleton: three absolutely-positioned 2px bars in a 22×16 box, so lines move independently without any flex reflow. What used to be four stylesheet blocks becomes four class lists:
<!-- Spin: outer bars fold, whole icon turns 180° -->
<span class="relative w-5.5 h-4 transition-[rotate] duration-450
group-aria-pressed:rotate-180">
<i class="… group-aria-pressed:translate-y-[7px] group-aria-pressed:rotate-45"></i>
<i class="… group-aria-pressed:opacity-0 group-aria-pressed:scale-x-0"></i>
<i class="… group-aria-pressed:-translate-y-[7px] group-aria-pressed:-rotate-45"></i>
</span>To ship one of these you copy one element. Nothing has to be deleted from a stylesheet, and two variants can coexist on a page without a naming scheme.
Beats are delays, and delays need the shorthand. Spin and Arrow are single-beat, so Tailwind's transition-[…] duration-* scale covers them. Squeeze converges the outer bars then rotates, and Drop lets the middle bar fall on an ease-in before the outers cross — two properties, two different delays, which the delay-* scale cannot express. Those two tiles use an arbitrary [transition:…] shorthand, and that is the honest boundary of the utility layer.
Why individual transform properties. rotate-45 and translate-y-[7px] compile to separate rotate and translate declarations, each independently transitionable. A single transform string would force one shared timing for the whole morph and make Squeeze impossible.
aria-pressed is the state. These are real toggle buttons: the script flips one attribute and never touches a class. Add aria-controls and they drive a real panel with no CSS change at all.
Make it yours
- Line geometry is three numbers: the
w-5.5 h-4box, theh-0.5bar, and thetop-[7px]mid offset. Scale all three together for a chunkier icon — the end-state translates reference the same offset. - Each variant's personality is its curve. Swap
ease-thb03forcubic-bezier(.3,1.4,.4,1)in@themeto add overshoot wobble everywhere at once. - Recolour the pressed state through
--color-thb03-acc; the tile border, the icon colour and the focus ring all read it. - Keep Arrow for drill-in navigation and pair it with a slide-in submenu — only the X-forming variants read as “close”.
- Drop the
<figcaption>recipe lines in production; they are teaching furniture.
Gotchas — read before shipping
- Put
transition-*on the base class list, never inside thegroup-aria-pressed:half, or the close animation plays with no easing — the classic one-way-animation bug. - Never animate
top-*. It is a layout property; that is why the bars are absolutely positioned from a static top and onlytranslate/rotatemove. - The middle bar must exit with
opacity-0 scale-x-0, not a width change — width animates layout and drags the neighbours' edges. - A bare icon button MUST carry
aria-label. Three decorative<i>elements announce as nothing at all. group-aria-pressed:requires thegroupclass on the button itself, not on the tile wrapper, or every tile morphs together.- Tailwind's
duration-*anddelay-*apply to all transitioned properties at once. Two different delays means the arbitrary[transition:…]shorthand; there is no way around it.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Individual transform properties (translate/rotate/scale) — which is what Tailwind v4 emits — are Baseline since Chrome 104, Safari 14.1 and Firefox 72. The floor quoted here is Tailwind v4's own generated sheet.