Tailwind + JSMIT licensed

Tailwind Hamburger to Close Button with aria-expanded Variants

Three bars become an ×. The port's win is that the animation now keys off the accessibility attribute the toggle already has to set: group-aria-expanded:rotate-45 reads aria-expanded directly, so the icon state and the announced state cannot drift.

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="tab-14 w-full min-h-svh bg-tab14-bg font-tab14-sans">
  <div id="tab14-shell" class="group" data-open="false">
    <div class="flex items-center justify-between px-6 py-4 border-b border-white/8">
      <span class="text-[1.1rem] font-bold tracking-[-0.01em] text-tab14-ink">MyApp</span>
      <button type="button" id="tab14-btn" aria-label="Toggle navigation" aria-expanded="false" aria-controls="tab14-menu" class="group/btn flex flex-col items-center justify-center gap-1.5 size-11 bg-white/6 border border-white/10 rounded-lg cursor-pointer transition-colors duration-[250ms] hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab14-acc aria-expanded:bg-tab14-acc/20 aria-expanded:border-tab14-acc/40">
        <span class="block w-6 h-0.5 rounded-sm bg-tab14-ink origin-center transition-[translate,rotate,scale,opacity,background-color] duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-aria-expanded/btn:translate-y-2 group-aria-expanded/btn:rotate-45 group-aria-expanded/btn:bg-tab14-acc motion-reduce:transition-none"></span>
        <span class="block w-6 h-0.5 rounded-sm bg-tab14-ink origin-center transition-[scale,opacity] duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-aria-expanded/btn:scale-x-0 group-aria-expanded/btn:opacity-0 motion-reduce:transition-none"></span>
        <span class="block w-6 h-0.5 rounded-sm bg-tab14-ink origin-center transition-[translate,rotate,scale,opacity,background-color] duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-aria-expanded/btn:-translate-y-2 group-aria-expanded/btn:-rotate-45 group-aria-expanded/btn:bg-tab14-acc motion-reduce:transition-none"></span>
      </button>
    </div>
    <nav id="tab14-menu" aria-label="Main" class="flex flex-col overflow-hidden max-h-0 px-6 opacity-0 transition-[max-height,opacity,padding] duration-[400ms] ease-[cubic-bezier(0.4,0,0.2,1)] group-data-[open=true]:max-h-52 group-data-[open=true]:opacity-100 group-data-[open=true]:py-4 motion-reduce:transition-none">
      <a href="#" class="flex items-center min-h-11 text-[0.95rem] font-semibold text-tab14-mut no-underline border-b border-white/5 transition-colors duration-200 hover:text-tab14-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab14-acc">Home</a>
      <a href="#" class="flex items-center min-h-11 text-[0.95rem] font-semibold text-tab14-mut no-underline border-b border-white/5 transition-colors duration-200 hover:text-tab14-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab14-acc">Features</a>
      <a href="#" class="flex items-center min-h-11 text-[0.95rem] font-semibold text-tab14-mut no-underline border-b border-white/5 transition-colors duration-200 hover:text-tab14-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab14-acc">Pricing</a>
      <a href="#" class="flex items-center min-h-11 text-[0.95rem] font-semibold text-tab14-mut no-underline transition-colors duration-200 hover:text-tab14-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab14-acc">Docs</a>
    </nav>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tab14-bg: #0f172a;
  --color-tab14-bg: oklch(0.21 0.04 258);
  --color-tab14-ink: #e2e8f0;
  --color-tab14-ink: oklch(0.92 0.01 250);
  --color-tab14-mut: #94a3b8;
  --color-tab14-mut: oklch(0.72 0.03 255);
  --color-tab14-acc: #a78bfa;
  --color-tab14-acc: oklch(0.74 0.15 292);
  --font-tab14-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tab-14 {
  width: 100%;
  min-height: 100svh;
}

.tab-14 ::selection {
  background: var(--color-tab14-acc);
  color: #14091f;
}
(function () {
  var shell = document.getElementById('tab14-shell');
  var btn = document.getElementById('tab14-btn');
  if (!shell || !btn) return;
  btn.addEventListener('click', function () {
    var open = btn.getAttribute('aria-expanded') !== 'true';
    btn.setAttribute('aria-expanded', String(open));
    shell.dataset.open = String(open);
  });
})();

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-tab14-bg: #0f172a;
  --color-tab14-bg: oklch(0.21 0.04 258);
  --color-tab14-ink: #e2e8f0;
  --color-tab14-ink: oklch(0.92 0.01 250);
  --color-tab14-mut: #94a3b8;
  --color-tab14-mut: oklch(0.72 0.03 255);
  --color-tab14-acc: #a78bfa;
  --color-tab14-acc: oklch(0.74 0.15 292);
  --font-tab14-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tab-14 {
  width: 100%;
  min-height: 100svh;
}

.tab-14 ::selection {
  background: var(--color-tab14-acc);
  color: #14091f;
}
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&display=swap">

What changed in the port

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

  • group-aria-expanded/btn: — a named group plus a built-in ARIA variant. The icon is now styled from the same attribute a screen reader reads, which is the strongest form of "style cannot drift from semantics".
  • The controller sets two attributes instead of toggling two classes, and one of them (aria-expanded) it had to set anyway. Net cost of the port: one line.
  • UI change from the CSS original: links are 44px tall and the panel is a real <nav> with aria-controls wiring. The original's 0.6rem-padded links were about 34px — under the touch minimum.
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 Close Button with aria-expanded Variants
Source: https://codefronts.com/motion/tailwind-css-animated-buttons/tailwind-hamburger-to-close-button/

Three bars become an ×. The port's win is that the animation now keys off the accessibility attribute the toggle already has to set: group-aria-expanded:rotate-45 reads aria-expanded directly, so the icon state and the announced state cannot drift.
## HTML
```html
<section class="tab-14 w-full min-h-svh bg-tab14-bg font-tab14-sans">
  <div id="tab14-shell" class="group" data-open="false">
    <div class="flex items-center justify-between px-6 py-4 border-b border-white/8">
      <span class="text-[1.1rem] font-bold tracking-[-0.01em] text-tab14-ink">MyApp</span>
      <button type="button" id="tab14-btn" aria-label="Toggle navigation" aria-expanded="false" aria-controls="tab14-menu" class="group/btn flex flex-col items-center justify-center gap-1.5 size-11 bg-white/6 border border-white/10 rounded-lg cursor-pointer transition-colors duration-[250ms] hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab14-acc aria-expanded:bg-tab14-acc/20 aria-expanded:border-tab14-acc/40">
        <span class="block w-6 h-0.5 rounded-sm bg-tab14-ink origin-center transition-[translate,rotate,scale,opacity,background-color] duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-aria-expanded/btn:translate-y-2 group-aria-expanded/btn:rotate-45 group-aria-expanded/btn:bg-tab14-acc motion-reduce:transition-none"></span>
        <span class="block w-6 h-0.5 rounded-sm bg-tab14-ink origin-center transition-[scale,opacity] duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-aria-expanded/btn:scale-x-0 group-aria-expanded/btn:opacity-0 motion-reduce:transition-none"></span>
        <span class="block w-6 h-0.5 rounded-sm bg-tab14-ink origin-center transition-[translate,rotate,scale,opacity,background-color] duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-aria-expanded/btn:-translate-y-2 group-aria-expanded/btn:-rotate-45 group-aria-expanded/btn:bg-tab14-acc motion-reduce:transition-none"></span>
      </button>
    </div>
    <nav id="tab14-menu" aria-label="Main" class="flex flex-col overflow-hidden max-h-0 px-6 opacity-0 transition-[max-height,opacity,padding] duration-[400ms] ease-[cubic-bezier(0.4,0,0.2,1)] group-data-[open=true]:max-h-52 group-data-[open=true]:opacity-100 group-data-[open=true]:py-4 motion-reduce:transition-none">
      <a href="#" class="flex items-center min-h-11 text-[0.95rem] font-semibold text-tab14-mut no-underline border-b border-white/5 transition-colors duration-200 hover:text-tab14-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab14-acc">Home</a>
      <a href="#" class="flex items-center min-h-11 text-[0.95rem] font-semibold text-tab14-mut no-underline border-b border-white/5 transition-colors duration-200 hover:text-tab14-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab14-acc">Features</a>
      <a href="#" class="flex items-center min-h-11 text-[0.95rem] font-semibold text-tab14-mut no-underline border-b border-white/5 transition-colors duration-200 hover:text-tab14-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab14-acc">Pricing</a>
      <a href="#" class="flex items-center min-h-11 text-[0.95rem] font-semibold text-tab14-mut no-underline transition-colors duration-200 hover:text-tab14-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab14-acc">Docs</a>
    </nav>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tab14-bg: #0f172a;
  --color-tab14-bg: oklch(0.21 0.04 258);
  --color-tab14-ink: #e2e8f0;
  --color-tab14-ink: oklch(0.92 0.01 250);
  --color-tab14-mut: #94a3b8;
  --color-tab14-mut: oklch(0.72 0.03 255);
  --color-tab14-acc: #a78bfa;
  --color-tab14-acc: oklch(0.74 0.15 292);
  --font-tab14-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tab-14 {
  width: 100%;
  min-height: 100svh;
}

.tab-14 ::selection {
  background: var(--color-tab14-acc);
  color: #14091f;
}
```

## JavaScript
```js
(function () {
  var shell = document.getElementById('tab14-shell');
  var btn = document.getElementById('tab14-btn');
  if (!shell || !btn) return;
  btn.addEventListener('click', function () {
    var open = btn.getAttribute('aria-expanded') !== 'true';
    btn.setAttribute('aria-expanded', String(open));
    shell.dataset.open = String(open);
  });
})();
```

How this works

The attribute is the state. A toggle must set aria-expanded for assistive tech regardless of styling. The CSS version then set .is-open as well — two sources of truth for one fact. Tailwind's aria-expanded: variant lets the bars read the attribute itself, so the controller sets one thing.

Bars are spans, not an SVG swap. The top bar drops 8px and rotates 45°, the bottom rises 8px and rotates -45°, the middle scales to nothing. The offsets are exactly the bar gap, which is why the cross closes cleanly at any bar width.

The panel is still a max-height transition, which needs a concrete open value (max-h-52). The wrapper carries data-open so the panel and the button can both be styled from one ancestor without either reaching into the other.

Make it yours

  • Bolder icon: widen the bars (w-6 → w-7) and keep the translate equal to the gap.
  • Spin the whole button on open with aria-expanded:rotate-90 on the button itself.
  • Swap the cross for a back-arrow by giving the top bar a shorter scale-x and a different rotation.

Gotchas — read before shipping

  • The translate must equal the distance between bar centres. Change gap-1.5 and the cross stops meeting at the middle.
  • max-h-52 has to exceed the panel's natural height or the last link clips; there is no max-h-auto that animates.
  • Do not use aria-expanded on the wrapper as well — only the control that expands something may carry it, which is why the panel keys off data-open instead.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Transform transitions and ARIA attribute selectors are universally supported; the floor is Tailwind v4's generated sheet.

Techniques used in this demo

Utilitiesfirst-class utility or variant

HTML & JSno Tailwind involved

Search CodeFronts

Loading…