28 Tailwind Hamburger Menus
03 / 28

All 28 in 28 Tailwind Hamburger Menus

  1. 01Tailwind Hamburger Menu with Clip-Path Circle Expansion
  2. 02Tailwind Keyboard Navigable Hamburger Menu with ARIA Attributes
  3. 03Tailwind Animated 3-Line to X Hamburger Icon Toggle
  4. 04Tailwind Slide-In Off-Canvas Drawer with Dimmed Backdrop Overlay
  5. 05Pure Tailwind Checkbox Hack Hamburger Navigation (No JavaScript)
  6. 06Tailwind Responsive Hamburger Menu with Integrated Search Bar & Social Links
  7. 07Tailwind Glassmorphism Backdrop Blur Hamburger Navigation
  8. 08Tailwind Push Page Navigation Menu (Off-Canvas Content Shift)
  9. 09Pure Tailwind Hamburger Menu Without JavaScript
  10. 10Tailwind Hamburger Menu Morphing to Close Icon Animation
  11. 11Tailwind Full Screen Mobile Navigation Overlay
  12. 12Tailwind Slide Out Sidebar Navigation / Off Canvas Menu
  13. 13Tailwind Responsive Navbar Mobile Hamburger Toggle Breakpoints
  14. 14Tailwind Minimalist Floating Hamburger Button
  15. 15Tailwind CSS Responsive Hamburger Menu Component
  16. 16Tailwind Rebuild of the Bootstrap 5 Navbar Hamburger Toggle
  17. 17Tailwind WCAG Accessible Hamburger Menu Example
  18. 18Pure Tailwind Dropdown Slide Down Mobile Hamburger Menu
  19. 19Simple 3-Line Tailwind Hamburger Icon Hover Effect
  20. 20Tailwind Full Screen Blur Overlay Hamburger Menu
  21. 21Tailwind Mobile Bottom Navigation Bar with Floating Hamburger Toggle
  22. 22Pure Tailwind Multi-Level Accordion Hamburger Dropdown Menu
  23. 23Flexbox Navbar Right-Aligned Tailwind Hamburger Icon
  24. 24Radial Ripple Expand Tailwind Hamburger Menu
  25. 25Sticky Tailwind Navbar Shrinking Hamburger Icon on Scroll
  26. 26Neumorphic Tailwind Hamburger Toggle Button
  27. 27Brutalist Design Bordered Tailwind Hamburger Menu
  28. 28Tailwind 3D Flip Canvas Mobile Hamburger Navigation
Tailwind + JSMIT licensed

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.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

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 &rarr; X morphs</h2>
      <span class="text-[11.5px] text-thb03-mut">click a tile &middot; 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&deg; &middot; fold 45&deg;</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 &rarr; 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 &middot; 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 &middot; 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;
}
(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.

1Load Tailwind
<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.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@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;
}
3Fonts used by this demooptional — skip if you have your own type stack
<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/.drop rule anywhere — each tile's bars carry their own group-aria-pressed: end states, so shipping a variant means copying one element rather than porting a stylesheet block.
  • [aria-pressed=&quot;true&quot;] .x becomes group-aria-pressed: on descendants and aria-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-0 and scale-x-[0.62] replace the scale:0 1 / scale:.62 1 pairs, and origin-left replaces the transform-origin rule.
  • The auto-fit tile grid needs no media query and no container query — it is one arbitrary grid-cols-[…] value on the wrapper.
  • The @media (prefers-reduced-motion) blanket rule becomes motion-reduce:transition-none repeated per bar. More verbose, but each element states its own fallback.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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 &mdash; Spin, Squeeze, Arrow and Drop &mdash; 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 &rarr; X morphs</h2>
      <span class="text-[11.5px] text-thb03-mut">click a tile &middot; 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&deg; &middot; fold 45&deg;</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 &rarr; 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 &middot; 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 &middot; 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-4 box, the h-0.5 bar, and the top-[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-thb03 for cubic-bezier(.3,1.4,.4,1) in @theme to 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 &ldquo;close&rdquo;.
  • Drop the <figcaption> recipe lines in production; they are teaching furniture.

Gotchas — read before shipping

  • Put transition-* on the base class list, never inside the group-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 only translate/rotate move.
  • 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 the group class on the button itself, not on the tile wrapper, or every tile morphs together.
  • Tailwind's duration-* and delay-* apply to all transitioned properties at once. Two different delays means the arbitrary [transition:…] shorthand; there is no way around it.

Browser support

ChromeSafariFirefoxEdge
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.

Techniques used in this demo

HTML & JSno Tailwind involved

Search CodeFronts

Loading…