Tailwind + JSMIT licensed

Tailwind Hamburger to X Menu Button Animation

Three bars become an ×: the top bar drops translate-y-2.5 and turns rotate-45, the middle one fades with scale-x-40, the bottom mirrors the top — all keyed off group-aria-expanded:. The menu wipes open with an animated clip-path, and the port fixes a real bug in the original: the collapsed links were still in the tab order. Now the closed nav is inert and Esc closes it.

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="tcb-08 w-full min-h-svh grid place-items-center bg-tcb08-bg px-6 py-12 font-tcb08-sans text-tcb08-ink" aria-label="Hamburger to X menu button demo">
  <div class="grid w-full justify-items-center gap-7">
    <div class="w-full max-w-[21rem]">
      <div class="flex items-center justify-between rounded-t-2xl border border-tcb08-line bg-tcb08-bar py-3 ps-5 pe-3">
        <span class="text-xs font-bold tracking-[0.16em]">NORTHWIND</span>
        <button id="tcb-08-btn" type="button" aria-expanded="false" aria-controls="tcb-08-nav" aria-label="Open menu" class="group flex size-11 cursor-pointer flex-col justify-center gap-2 rounded-xl p-2.5 transition-colors hover:bg-white/7 focus-visible:outline-[2.5px] focus-visible:outline-offset-2 focus-visible:outline-tcb08-acc motion-reduce:transition-none">
          <span aria-hidden="true" class="block h-0.5 w-full rounded-full bg-tcb08-ink transition duration-300 ease-[cubic-bezier(.4,0,.2,1)] group-aria-expanded:translate-y-2.5 group-aria-expanded:rotate-45 motion-reduce:transition-none"></span>
          <span aria-hidden="true" class="block h-0.5 w-full rounded-full bg-tcb08-ink transition duration-200 group-aria-expanded:scale-x-40 group-aria-expanded:opacity-0 motion-reduce:transition-none"></span>
          <span aria-hidden="true" class="block h-0.5 w-full rounded-full bg-tcb08-ink transition duration-300 ease-[cubic-bezier(.4,0,.2,1)] group-aria-expanded:-translate-y-2.5 group-aria-expanded:-rotate-45 motion-reduce:transition-none"></span>
        </button>
      </div>
      <nav id="tcb-08-nav" aria-label="Main" class="flex flex-col overflow-hidden rounded-b-2xl border border-t-0 border-tcb08-line bg-tcb08-panel transition-[clip-path] duration-350 ease-[cubic-bezier(.4,0,.2,1)] [clip-path:inset(0_0_100%_0)] data-open:[clip-path:inset(0)] motion-reduce:transition-none">
        <a href="#" class="flex min-h-12 items-center justify-between border-t border-tcb08-line/60 px-5 text-sm transition-colors hover:bg-white/5 focus-visible:outline-2 focus-visible:-outline-offset-3 focus-visible:outline-tcb08-acc">Work <span aria-hidden="true" class="text-tcb08-mut">→</span></a>
        <a href="#" class="flex min-h-12 items-center justify-between border-t border-tcb08-line/60 px-5 text-sm transition-colors hover:bg-white/5 focus-visible:outline-2 focus-visible:-outline-offset-3 focus-visible:outline-tcb08-acc">Studio <span aria-hidden="true" class="text-tcb08-mut">→</span></a>
        <a href="#" class="flex min-h-12 items-center justify-between border-t border-tcb08-line/60 px-5 text-sm transition-colors hover:bg-white/5 focus-visible:outline-2 focus-visible:-outline-offset-3 focus-visible:outline-tcb08-acc">Contact <span aria-hidden="true" class="text-tcb08-mut">→</span></a>
      </nav>
    </div>
    <p aria-hidden="true" class="rounded-full px-3.5 py-2 font-tcb08-mono text-[11.5px] text-tcb08-mut ring-1 ring-white/12">group-aria-expanded:rotate-45 · data-open:[clip-path:inset(0)] · inert</p>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tcb08-bg: #13151f;
  --color-tcb08-bg: oklch(0.18 0.025 270);
  --color-tcb08-bar: #1c1f2c;
  --color-tcb08-bar: oklch(0.23 0.03 270);
  --color-tcb08-panel: #191b27;
  --color-tcb08-panel: oklch(0.21 0.028 270);
  --color-tcb08-line: #3a3e50;
  --color-tcb08-line: oklch(0.35 0.03 270);
  --color-tcb08-ink: #e7e8ef;
  --color-tcb08-ink: oklch(0.93 0.01 270);
  --color-tcb08-mut: #a3a6b8;
  --color-tcb08-mut: oklch(0.74 0.025 270);
  --color-tcb08-acc: #9197f2;
  --color-tcb08-acc: oklch(0.7 0.15 275);
  --font-tcb08-sans: 'Geist', system-ui, sans-serif;
  --font-tcb08-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tcb-08 {
  width: 100%;
  min-height: 100svh;
}
(() => {
  const btn = document.getElementById('tcb-08-btn');
  if (!btn || btn.dataset.wired) return;
  btn.dataset.wired = '1';
  const nav = document.getElementById('tcb-08-nav');
  const set = (open) => {
    btn.setAttribute('aria-expanded', String(open));
    btn.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
    nav.toggleAttribute('data-open', open);
    nav.inert = !open;
  };
  set(false);
  btn.addEventListener('click', () => set(btn.getAttribute('aria-expanded') !== 'true'));
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && btn.getAttribute('aria-expanded') === 'true') { set(false); btn.focus(); }
  });
})();

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-tcb08-bg: #13151f;
  --color-tcb08-bg: oklch(0.18 0.025 270);
  --color-tcb08-bar: #1c1f2c;
  --color-tcb08-bar: oklch(0.23 0.03 270);
  --color-tcb08-panel: #191b27;
  --color-tcb08-panel: oklch(0.21 0.028 270);
  --color-tcb08-line: #3a3e50;
  --color-tcb08-line: oklch(0.35 0.03 270);
  --color-tcb08-ink: #e7e8ef;
  --color-tcb08-ink: oklch(0.93 0.01 270);
  --color-tcb08-mut: #a3a6b8;
  --color-tcb08-mut: oklch(0.74 0.025 270);
  --color-tcb08-acc: #9197f2;
  --color-tcb08-acc: oklch(0.7 0.15 275);
  --font-tcb08-sans: 'Geist', system-ui, sans-serif;
  --font-tcb08-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tcb-08 {
  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=Geist:wght@400..800&family=Geist+Mono:wght@400..600&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • Three [aria-expanded="true"] .bar:nth-child(n) rules became group-aria-expanded: utilities on each bar; calc(var(--gap) + 2px) became translate-y-2.5.
  • Bug fix: in the CSS original the clipped-away links were still focusable, so Tab walked into an invisible menu. The closed nav is now inert.
  • UI upgrade from the CSS original: Esc closes the menu and returns focus to the toggle, links grew to 48px rows with a trailing arrow, and the nav has an aria-label.
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 Hamburger to X Menu Button Animation
Source: https://codefronts.com/components/tailwind-css-close-buttons/tailwind-hamburger-to-x-menu-button-animation/

Three bars become an ×: the top bar drops translate-y-2.5 and turns rotate-45, the middle one fades with scale-x-40, the bottom mirrors the top — all keyed off group-aria-expanded:. The menu wipes open with an animated clip-path, and the port fixes a real bug in the original: the collapsed links were still in the tab order. Now the closed nav is inert and Esc closes it.
## HTML
```html
<section class="tcb-08 w-full min-h-svh grid place-items-center bg-tcb08-bg px-6 py-12 font-tcb08-sans text-tcb08-ink" aria-label="Hamburger to X menu button demo">
  <div class="grid w-full justify-items-center gap-7">
    <div class="w-full max-w-[21rem]">
      <div class="flex items-center justify-between rounded-t-2xl border border-tcb08-line bg-tcb08-bar py-3 ps-5 pe-3">
        <span class="text-xs font-bold tracking-[0.16em]">NORTHWIND</span>
        <button id="tcb-08-btn" type="button" aria-expanded="false" aria-controls="tcb-08-nav" aria-label="Open menu" class="group flex size-11 cursor-pointer flex-col justify-center gap-2 rounded-xl p-2.5 transition-colors hover:bg-white/7 focus-visible:outline-[2.5px] focus-visible:outline-offset-2 focus-visible:outline-tcb08-acc motion-reduce:transition-none">
          <span aria-hidden="true" class="block h-0.5 w-full rounded-full bg-tcb08-ink transition duration-300 ease-[cubic-bezier(.4,0,.2,1)] group-aria-expanded:translate-y-2.5 group-aria-expanded:rotate-45 motion-reduce:transition-none"></span>
          <span aria-hidden="true" class="block h-0.5 w-full rounded-full bg-tcb08-ink transition duration-200 group-aria-expanded:scale-x-40 group-aria-expanded:opacity-0 motion-reduce:transition-none"></span>
          <span aria-hidden="true" class="block h-0.5 w-full rounded-full bg-tcb08-ink transition duration-300 ease-[cubic-bezier(.4,0,.2,1)] group-aria-expanded:-translate-y-2.5 group-aria-expanded:-rotate-45 motion-reduce:transition-none"></span>
        </button>
      </div>
      <nav id="tcb-08-nav" aria-label="Main" class="flex flex-col overflow-hidden rounded-b-2xl border border-t-0 border-tcb08-line bg-tcb08-panel transition-[clip-path] duration-350 ease-[cubic-bezier(.4,0,.2,1)] [clip-path:inset(0_0_100%_0)] data-open:[clip-path:inset(0)] motion-reduce:transition-none">
        <a href="#" class="flex min-h-12 items-center justify-between border-t border-tcb08-line/60 px-5 text-sm transition-colors hover:bg-white/5 focus-visible:outline-2 focus-visible:-outline-offset-3 focus-visible:outline-tcb08-acc">Work <span aria-hidden="true" class="text-tcb08-mut">→</span></a>
        <a href="#" class="flex min-h-12 items-center justify-between border-t border-tcb08-line/60 px-5 text-sm transition-colors hover:bg-white/5 focus-visible:outline-2 focus-visible:-outline-offset-3 focus-visible:outline-tcb08-acc">Studio <span aria-hidden="true" class="text-tcb08-mut">→</span></a>
        <a href="#" class="flex min-h-12 items-center justify-between border-t border-tcb08-line/60 px-5 text-sm transition-colors hover:bg-white/5 focus-visible:outline-2 focus-visible:-outline-offset-3 focus-visible:outline-tcb08-acc">Contact <span aria-hidden="true" class="text-tcb08-mut">→</span></a>
      </nav>
    </div>
    <p aria-hidden="true" class="rounded-full px-3.5 py-2 font-tcb08-mono text-[11.5px] text-tcb08-mut ring-1 ring-white/12">group-aria-expanded:rotate-45 · data-open:[clip-path:inset(0)] · inert</p>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tcb08-bg: #13151f;
  --color-tcb08-bg: oklch(0.18 0.025 270);
  --color-tcb08-bar: #1c1f2c;
  --color-tcb08-bar: oklch(0.23 0.03 270);
  --color-tcb08-panel: #191b27;
  --color-tcb08-panel: oklch(0.21 0.028 270);
  --color-tcb08-line: #3a3e50;
  --color-tcb08-line: oklch(0.35 0.03 270);
  --color-tcb08-ink: #e7e8ef;
  --color-tcb08-ink: oklch(0.93 0.01 270);
  --color-tcb08-mut: #a3a6b8;
  --color-tcb08-mut: oklch(0.74 0.025 270);
  --color-tcb08-acc: #9197f2;
  --color-tcb08-acc: oklch(0.7 0.15 275);
  --font-tcb08-sans: 'Geist', system-ui, sans-serif;
  --font-tcb08-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tcb-08 {
  width: 100%;
  min-height: 100svh;
}
```

## JavaScript
```js
(() => {
  const btn = document.getElementById('tcb-08-btn');
  if (!btn || btn.dataset.wired) return;
  btn.dataset.wired = '1';
  const nav = document.getElementById('tcb-08-nav');
  const set = (open) => {
    btn.setAttribute('aria-expanded', String(open));
    btn.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
    nav.toggleAttribute('data-open', open);
    nav.inert = !open;
  };
  set(false);
  btn.addEventListener('click', () => set(btn.getAttribute('aria-expanded') !== 'true'));
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && btn.getAttribute('aria-expanded') === 'true') { set(false); btn.focus(); }
  });
})();
```

How this works

The travel distance is the gap plus the bar. Bars are 2px tall with an 8px gap, so the outer bars travel 10px — translate-y-2.5 — to meet the middle one before rotating:

bar 1: group-aria-expanded:translate-y-2.5 group-aria-expanded:rotate-45
bar 2: group-aria-expanded:opacity-0 group-aria-expanded:scale-x-40
bar 3: group-aria-expanded:-translate-y-2.5 group-aria-expanded:-rotate-45

Why the port needs no calc(). The CSS original computed calc(var(--gap) + 2px); in utilities the gap is gap-2 and the travel is its matching step, readable at a glance.

Make it yours

  • Thicker bars: h-[3px] and change travel to translate-y-[11px] (gap + bar height).
  • Arrow instead of ×: rotate only the outer bars ±40° and shorten them with scale-x-60.
  • Full-screen menu: replace the dropdown with a fixed inset-0 sheet — the toggle doesn't change.

Gotchas — read before shipping

  • If you change the gap, change the translate: they must stay equal to gap + bar height or the × misaligns.
  • clip-path hides visually but not from assistive tech — that is why inert is required.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

The floor is Tailwind v4; inert is universal on it.

Techniques used in this demo

Arbitraryneeds bracket syntax

HTML & JSno Tailwind involved

Search CodeFronts

Loading…