28 Tailwind Hamburger Menus
02 / 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 Keyboard Navigable Hamburger Menu with ARIA Attributes

The WAI-ARIA disclosure-navigation pattern in utilities, and the port has a genuine win: because the open state IS aria-expanded, aria-expanded: and group-has-aria-expanded: style straight off the accessibility attribute. Style and semantics cannot drift, because there is only one source of truth. Arrow/Home/End roving focus, Escape that returns focus, no focus trap — a disclosure, not a dialog.

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-02 w-full min-h-svh bg-thb02-bg font-thb02-sans text-thb02-ink px-5 py-9" aria-label="Keyboard navigable ARIA hamburger menu demo">
  <div class="min-h-105 flex flex-col gap-4.5 p-6.5 rounded-[18px] border border-thb02-edge bg-thb02-bg">
    <div class="group w-[min(440px,100%)] mx-auto overflow-hidden rounded-2xl border border-thb02-edge bg-thb02-panel shadow-[0_16px_40px_-28px_oklch(0.35_0.05_260/0.5)]">
      <header class="flex items-center justify-between py-3 pl-4.5 pr-3">
        <span class="flex items-center gap-2.25 text-[15px] font-bold tracking-[-0.01em]">
          <i class="w-4.5 h-4.5 rounded-[5px] bg-[conic-gradient(from_180deg,var(--color-thb02-acc),oklch(0.7_0.13_200))]" aria-hidden="true"></i>Fieldnote
        </span>
        <button type="button" aria-expanded="false" aria-controls="thb02-panel" class="group flex items-center gap-2.5 min-h-11 px-4 rounded-[11px] border border-thb02-edge bg-thb02-panel font-thb02-sans text-[13.5px] font-semibold text-thb02-ink cursor-pointer transition-[background,border-color] duration-200 hover:bg-thb02-hover hover:border-thb02-edge2 focus-visible:outline-2 focus-visible:outline-thb02-acc focus-visible:outline-offset-2">
          <span class="relative block w-4 h-3" aria-hidden="true">
            <i class="absolute left-0 top-0 w-4 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-250 group-aria-expanded:translate-y-1.25 group-aria-expanded:rotate-45 motion-reduce:transition-none"></i>
            <i class="absolute left-0 top-1.25 w-4 h-0.5 rounded-sm bg-current transition-[opacity,translate] duration-200 group-aria-expanded:opacity-0 group-aria-expanded:-translate-x-1.25 motion-reduce:transition-none"></i>
            <i class="absolute left-0 top-2.5 w-4 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-250 group-aria-expanded:-translate-y-1.25 group-aria-expanded:-rotate-45 motion-reduce:transition-none"></i>
          </span>Menu
        </button>
      </header>
      <nav id="thb02-panel" aria-label="Primary" class="grid opacity-0 [grid-template-rows:0fr] transition-[grid-template-rows,opacity] duration-350 ease-thb02 group-has-aria-expanded:opacity-100 group-has-aria-expanded:[grid-template-rows:1fr] motion-reduce:transition-none">
        <ul class="list-none min-h-0 overflow-hidden flex flex-col px-2 group-has-aria-expanded:pb-2 group-has-aria-expanded:border-t group-has-aria-expanded:border-thb02-edge">
          <li><a href="#dashboard" aria-current="page" class="flex items-center min-h-11.5 mt-1 px-3 rounded-[9px] no-underline text-sm font-semibold text-thb02-mut transition-[background,color] duration-150 hover:bg-thb02-hover hover:text-thb02-ink focus-visible:text-thb02-ink focus-visible:outline-2 focus-visible:outline-thb02-acc focus-visible:-outline-offset-2 aria-current:text-thb02-acc aria-current:bg-[color-mix(in_oklch,var(--color-thb02-acc)_9%,transparent)]">Dashboard</a></li>
          <li><a href="#projects" class="flex items-center min-h-11.5 mt-1 px-3 rounded-[9px] no-underline text-sm font-semibold text-thb02-mut transition-[background,color] duration-150 hover:bg-thb02-hover hover:text-thb02-ink focus-visible:text-thb02-ink focus-visible:outline-2 focus-visible:outline-thb02-acc focus-visible:-outline-offset-2">Projects</a></li>
          <li><a href="#reports" class="flex items-center min-h-11.5 mt-1 px-3 rounded-[9px] no-underline text-sm font-semibold text-thb02-mut transition-[background,color] duration-150 hover:bg-thb02-hover hover:text-thb02-ink focus-visible:text-thb02-ink focus-visible:outline-2 focus-visible:outline-thb02-acc focus-visible:-outline-offset-2">Reports</a></li>
          <li><a href="#team" class="flex items-center min-h-11.5 mt-1 px-3 rounded-[9px] no-underline text-sm font-semibold text-thb02-mut transition-[background,color] duration-150 hover:bg-thb02-hover hover:text-thb02-ink focus-visible:text-thb02-ink focus-visible:outline-2 focus-visible:outline-thb02-acc focus-visible:-outline-offset-2">Team</a></li>
          <li><a href="#settings" class="flex items-center min-h-11.5 mt-1 px-3 rounded-[9px] no-underline text-sm font-semibold text-thb02-mut transition-[background,color] duration-150 hover:bg-thb02-hover hover:text-thb02-ink focus-visible:text-thb02-ink focus-visible:outline-2 focus-visible:outline-thb02-acc focus-visible:-outline-offset-2">Settings</a></li>
        </ul>
      </nav>
    </div>
    <p class="w-[min(440px,100%)] mx-auto text-center text-xs leading-[2.1] text-thb02-mut text-pretty" aria-hidden="true">
      <kbd class="inline-block min-w-6.5 mx-0.5 px-1.5 py-0.5 text-center font-thb02-mono text-[11px] font-semibold text-thb02-ink bg-thb02-panel border border-b-2 border-thb02-edge rounded-md">Tab</kbd> reach button &middot;
      <kbd class="inline-block min-w-6.5 mx-0.5 px-1.5 py-0.5 text-center font-thb02-mono text-[11px] font-semibold text-thb02-ink bg-thb02-panel border border-b-2 border-thb02-edge rounded-md">Enter</kbd> open &middot;
      <kbd class="inline-block min-w-6.5 mx-0.5 px-1.5 py-0.5 text-center font-thb02-mono text-[11px] font-semibold text-thb02-ink bg-thb02-panel border border-b-2 border-thb02-edge rounded-md">&darr;</kbd><kbd class="inline-block min-w-6.5 mx-0.5 px-1.5 py-0.5 text-center font-thb02-mono text-[11px] font-semibold text-thb02-ink bg-thb02-panel border border-b-2 border-thb02-edge rounded-md">&uarr;</kbd> move &middot;
      <kbd class="inline-block min-w-6.5 mx-0.5 px-1.5 py-0.5 text-center font-thb02-mono text-[11px] font-semibold text-thb02-ink bg-thb02-panel border border-b-2 border-thb02-edge rounded-md">Home</kbd><kbd class="inline-block min-w-6.5 mx-0.5 px-1.5 py-0.5 text-center font-thb02-mono text-[11px] font-semibold text-thb02-ink bg-thb02-panel border border-b-2 border-thb02-edge rounded-md">End</kbd> jump &middot;
      <kbd class="inline-block min-w-6.5 mx-0.5 px-1.5 py-0.5 text-center font-thb02-mono text-[11px] font-semibold text-thb02-ink bg-thb02-panel border border-b-2 border-thb02-edge rounded-md">Esc</kbd> close &amp; return
    </p>
    <output class="w-[min(440px,100%)] mx-auto text-center font-thb02-mono text-xs text-thb02-mut" aria-hidden="true">aria-expanded: <b class="text-thb02-acc">false</b></output>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-thb02-bg: #fafafc;
  --color-thb02-bg: oklch(0.985 0.003 250);
  --color-thb02-panel: #ffffff;
  --color-thb02-panel: oklch(1 0 0);
  --color-thb02-hover: #f3f4f7;
  --color-thb02-hover: oklch(0.965 0.006 250);
  --color-thb02-edge: #e3e5ea;
  --color-thb02-edge: oklch(0.9 0.01 250);
  --color-thb02-edge2: #c6c9d1;
  --color-thb02-edge2: oklch(0.8 0.02 255);
  --color-thb02-ink: #33384a;
  --color-thb02-ink: oklch(0.25 0.02 255);
  --color-thb02-mut: #74798a;
  --color-thb02-mut: oklch(0.54 0.015 255);
  --color-thb02-acc: #4a5fd0;
  --color-thb02-acc: oklch(0.52 0.17 262);
  --font-thb02-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb02-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb02: cubic-bezier(0.2, 0.7, 0.2, 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-02 {
  width: 100%;
  min-height: 100svh;
}
(function () {
  document.querySelectorAll('.thb-02 div.group').forEach(function (bar) {
    if (bar.dataset.bound) return; bar.dataset.bound = '1';
    var btn = bar.querySelector('button[aria-controls="thb02-panel"]'),
        panel = bar.querySelector('#thb02-panel'),
        out = bar.closest('.thb-02').querySelector('output b'),
        links = Array.prototype.slice.call(panel.querySelectorAll('a'));
    function set(open, refocus) {
      btn.setAttribute('aria-expanded', String(open));
      if (out) out.textContent = String(open);
      if (open) links[0].focus();
      else if (refocus) btn.focus();
    }
    btn.addEventListener('click', function () { set(btn.getAttribute('aria-expanded') !== 'true'); });
    bar.addEventListener('keydown', function (e) {
      var open = btn.getAttribute('aria-expanded') === 'true';
      if (e.key === 'Escape' && open) { set(false, true); return; }
      var i = links.indexOf(document.activeElement);
      if (i < 0) return;
      var next = { ArrowDown: i + 1, ArrowUp: i - 1, Home: 0, End: links.length - 1 }[e.key];
      if (next === undefined) return;
      e.preventDefault();
      links[(next + links.length) % links.length].focus();
    });
    bar.addEventListener('focusout', function () {
      requestAnimationFrame(function () {
        if (!bar.contains(document.activeElement)) set(false, false);
      });
    });
  });
})();

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-thb02-bg: #fafafc;
  --color-thb02-bg: oklch(0.985 0.003 250);
  --color-thb02-panel: #ffffff;
  --color-thb02-panel: oklch(1 0 0);
  --color-thb02-hover: #f3f4f7;
  --color-thb02-hover: oklch(0.965 0.006 250);
  --color-thb02-edge: #e3e5ea;
  --color-thb02-edge: oklch(0.9 0.01 250);
  --color-thb02-edge2: #c6c9d1;
  --color-thb02-edge2: oklch(0.8 0.02 255);
  --color-thb02-ink: #33384a;
  --color-thb02-ink: oklch(0.25 0.02 255);
  --color-thb02-mut: #74798a;
  --color-thb02-mut: oklch(0.54 0.015 255);
  --color-thb02-acc: #4a5fd0;
  --color-thb02-acc: oklch(0.52 0.17 262);
  --font-thb02-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb02-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb02: cubic-bezier(0.2, 0.7, 0.2, 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-02 {
  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 clearest win in the collection: :has([aria-expanded=&quot;true&quot;]) becomes group-has-aria-expanded:, and the icon morph becomes aria-expanded: on the button itself. No mirrored data-open attribute exists, so the visual state cannot disagree with the accessibility state.
  • grid-template-rows:0fr has no utility and should not — a fractional row track is not a design-scale value. Both ends stay arbitrary properties on the element.
  • aria-current: replaces the a[aria-current=&quot;page&quot;] rule, and the tint is an inline color-mix() reading the accent token, so re-theming is still one line.
  • Six @theme colour entries replace the CSS version's --tokens block and buy the whole bg-/text-/border- family for each.
  • The <kbd> legend repeats a long class list six times. That is the honest cost of utilities on repeated inline elements; a component wrapper is the fix in a real codebase, not @apply.
  • Escape, roving focus and the focusout close stay in JS. Keyboard behaviour is not a styling concern and has no variant form.
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 Keyboard Navigable Hamburger Menu with ARIA Attributes
Source: https://codefronts.com/navigation/tailwind-hamburger-menus/tailwind-keyboard-navigable-hamburger-menu-aria-attributes/

The WAI-ARIA disclosure-navigation pattern in utilities, and the port has a genuine win: because the open state IS aria-expanded, aria-expanded: and group-has-aria-expanded: style straight off the accessibility attribute. Style and semantics cannot drift, because there is only one source of truth. Arrow/Home/End roving focus, Escape that returns focus, no focus trap &mdash; a disclosure, not a dialog.
## HTML
```html
<section class="thb-02 w-full min-h-svh bg-thb02-bg font-thb02-sans text-thb02-ink px-5 py-9" aria-label="Keyboard navigable ARIA hamburger menu demo">
  <div class="min-h-105 flex flex-col gap-4.5 p-6.5 rounded-[18px] border border-thb02-edge bg-thb02-bg">
    <div class="group w-[min(440px,100%)] mx-auto overflow-hidden rounded-2xl border border-thb02-edge bg-thb02-panel shadow-[0_16px_40px_-28px_oklch(0.35_0.05_260/0.5)]">
      <header class="flex items-center justify-between py-3 pl-4.5 pr-3">
        <span class="flex items-center gap-2.25 text-[15px] font-bold tracking-[-0.01em]">
          <i class="w-4.5 h-4.5 rounded-[5px] bg-[conic-gradient(from_180deg,var(--color-thb02-acc),oklch(0.7_0.13_200))]" aria-hidden="true"></i>Fieldnote
        </span>
        <button type="button" aria-expanded="false" aria-controls="thb02-panel" class="group flex items-center gap-2.5 min-h-11 px-4 rounded-[11px] border border-thb02-edge bg-thb02-panel font-thb02-sans text-[13.5px] font-semibold text-thb02-ink cursor-pointer transition-[background,border-color] duration-200 hover:bg-thb02-hover hover:border-thb02-edge2 focus-visible:outline-2 focus-visible:outline-thb02-acc focus-visible:outline-offset-2">
          <span class="relative block w-4 h-3" aria-hidden="true">
            <i class="absolute left-0 top-0 w-4 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-250 group-aria-expanded:translate-y-1.25 group-aria-expanded:rotate-45 motion-reduce:transition-none"></i>
            <i class="absolute left-0 top-1.25 w-4 h-0.5 rounded-sm bg-current transition-[opacity,translate] duration-200 group-aria-expanded:opacity-0 group-aria-expanded:-translate-x-1.25 motion-reduce:transition-none"></i>
            <i class="absolute left-0 top-2.5 w-4 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-250 group-aria-expanded:-translate-y-1.25 group-aria-expanded:-rotate-45 motion-reduce:transition-none"></i>
          </span>Menu
        </button>
      </header>
      <nav id="thb02-panel" aria-label="Primary" class="grid opacity-0 [grid-template-rows:0fr] transition-[grid-template-rows,opacity] duration-350 ease-thb02 group-has-aria-expanded:opacity-100 group-has-aria-expanded:[grid-template-rows:1fr] motion-reduce:transition-none">
        <ul class="list-none min-h-0 overflow-hidden flex flex-col px-2 group-has-aria-expanded:pb-2 group-has-aria-expanded:border-t group-has-aria-expanded:border-thb02-edge">
          <li><a href="#dashboard" aria-current="page" class="flex items-center min-h-11.5 mt-1 px-3 rounded-[9px] no-underline text-sm font-semibold text-thb02-mut transition-[background,color] duration-150 hover:bg-thb02-hover hover:text-thb02-ink focus-visible:text-thb02-ink focus-visible:outline-2 focus-visible:outline-thb02-acc focus-visible:-outline-offset-2 aria-current:text-thb02-acc aria-current:bg-[color-mix(in_oklch,var(--color-thb02-acc)_9%,transparent)]">Dashboard</a></li>
          <li><a href="#projects" class="flex items-center min-h-11.5 mt-1 px-3 rounded-[9px] no-underline text-sm font-semibold text-thb02-mut transition-[background,color] duration-150 hover:bg-thb02-hover hover:text-thb02-ink focus-visible:text-thb02-ink focus-visible:outline-2 focus-visible:outline-thb02-acc focus-visible:-outline-offset-2">Projects</a></li>
          <li><a href="#reports" class="flex items-center min-h-11.5 mt-1 px-3 rounded-[9px] no-underline text-sm font-semibold text-thb02-mut transition-[background,color] duration-150 hover:bg-thb02-hover hover:text-thb02-ink focus-visible:text-thb02-ink focus-visible:outline-2 focus-visible:outline-thb02-acc focus-visible:-outline-offset-2">Reports</a></li>
          <li><a href="#team" class="flex items-center min-h-11.5 mt-1 px-3 rounded-[9px] no-underline text-sm font-semibold text-thb02-mut transition-[background,color] duration-150 hover:bg-thb02-hover hover:text-thb02-ink focus-visible:text-thb02-ink focus-visible:outline-2 focus-visible:outline-thb02-acc focus-visible:-outline-offset-2">Team</a></li>
          <li><a href="#settings" class="flex items-center min-h-11.5 mt-1 px-3 rounded-[9px] no-underline text-sm font-semibold text-thb02-mut transition-[background,color] duration-150 hover:bg-thb02-hover hover:text-thb02-ink focus-visible:text-thb02-ink focus-visible:outline-2 focus-visible:outline-thb02-acc focus-visible:-outline-offset-2">Settings</a></li>
        </ul>
      </nav>
    </div>
    <p class="w-[min(440px,100%)] mx-auto text-center text-xs leading-[2.1] text-thb02-mut text-pretty" aria-hidden="true">
      <kbd class="inline-block min-w-6.5 mx-0.5 px-1.5 py-0.5 text-center font-thb02-mono text-[11px] font-semibold text-thb02-ink bg-thb02-panel border border-b-2 border-thb02-edge rounded-md">Tab</kbd> reach button &middot;
      <kbd class="inline-block min-w-6.5 mx-0.5 px-1.5 py-0.5 text-center font-thb02-mono text-[11px] font-semibold text-thb02-ink bg-thb02-panel border border-b-2 border-thb02-edge rounded-md">Enter</kbd> open &middot;
      <kbd class="inline-block min-w-6.5 mx-0.5 px-1.5 py-0.5 text-center font-thb02-mono text-[11px] font-semibold text-thb02-ink bg-thb02-panel border border-b-2 border-thb02-edge rounded-md">&darr;</kbd><kbd class="inline-block min-w-6.5 mx-0.5 px-1.5 py-0.5 text-center font-thb02-mono text-[11px] font-semibold text-thb02-ink bg-thb02-panel border border-b-2 border-thb02-edge rounded-md">&uarr;</kbd> move &middot;
      <kbd class="inline-block min-w-6.5 mx-0.5 px-1.5 py-0.5 text-center font-thb02-mono text-[11px] font-semibold text-thb02-ink bg-thb02-panel border border-b-2 border-thb02-edge rounded-md">Home</kbd><kbd class="inline-block min-w-6.5 mx-0.5 px-1.5 py-0.5 text-center font-thb02-mono text-[11px] font-semibold text-thb02-ink bg-thb02-panel border border-b-2 border-thb02-edge rounded-md">End</kbd> jump &middot;
      <kbd class="inline-block min-w-6.5 mx-0.5 px-1.5 py-0.5 text-center font-thb02-mono text-[11px] font-semibold text-thb02-ink bg-thb02-panel border border-b-2 border-thb02-edge rounded-md">Esc</kbd> close &amp; return
    </p>
    <output class="w-[min(440px,100%)] mx-auto text-center font-thb02-mono text-xs text-thb02-mut" aria-hidden="true">aria-expanded: <b class="text-thb02-acc">false</b></output>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-thb02-bg: #fafafc;
  --color-thb02-bg: oklch(0.985 0.003 250);
  --color-thb02-panel: #ffffff;
  --color-thb02-panel: oklch(1 0 0);
  --color-thb02-hover: #f3f4f7;
  --color-thb02-hover: oklch(0.965 0.006 250);
  --color-thb02-edge: #e3e5ea;
  --color-thb02-edge: oklch(0.9 0.01 250);
  --color-thb02-edge2: #c6c9d1;
  --color-thb02-edge2: oklch(0.8 0.02 255);
  --color-thb02-ink: #33384a;
  --color-thb02-ink: oklch(0.25 0.02 255);
  --color-thb02-mut: #74798a;
  --color-thb02-mut: oklch(0.54 0.015 255);
  --color-thb02-acc: #4a5fd0;
  --color-thb02-acc: oklch(0.52 0.17 262);
  --font-thb02-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb02-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb02: cubic-bezier(0.2, 0.7, 0.2, 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-02 {
  width: 100%;
  min-height: 100svh;
}
```

## JavaScript
```js
(function () {
  document.querySelectorAll('.thb-02 div.group').forEach(function (bar) {
    if (bar.dataset.bound) return; bar.dataset.bound = '1';
    var btn = bar.querySelector('button[aria-controls="thb02-panel"]'),
        panel = bar.querySelector('#thb02-panel'),
        out = bar.closest('.thb-02').querySelector('output b'),
        links = Array.prototype.slice.call(panel.querySelectorAll('a'));
    function set(open, refocus) {
      btn.setAttribute('aria-expanded', String(open));
      if (out) out.textContent = String(open);
      if (open) links[0].focus();
      else if (refocus) btn.focus();
    }
    btn.addEventListener('click', function () { set(btn.getAttribute('aria-expanded') !== 'true'); });
    bar.addEventListener('keydown', function (e) {
      var open = btn.getAttribute('aria-expanded') === 'true';
      if (e.key === 'Escape' && open) { set(false, true); return; }
      var i = links.indexOf(document.activeElement);
      if (i < 0) return;
      var next = { ArrowDown: i + 1, ArrowUp: i - 1, Home: 0, End: links.length - 1 }[e.key];
      if (next === undefined) return;
      e.preventDefault();
      links[(next + links.length) % links.length].focus();
    });
    bar.addEventListener('focusout', function () {
      requestAnimationFrame(function () {
        if (!bar.contains(document.activeElement)) set(false, false);
      });
    });
  });
})();
```

How this works

The accessibility attribute is the state. The CSS version keys its open styles off :has([aria-expanded="true"]). Tailwind has that as a first-class variant, so it goes on the elements that respond:

<div class="group">
  <button aria-expanded="false" aria-controls="thb02-panel">…</button>
  <nav id="thb02-panel"
    class="grid [grid-template-rows:0fr] opacity-0
           group-has-aria-expanded:[grid-template-rows:1fr]
           group-has-aria-expanded:opacity-100">

There is no data-open anywhere. The script sets exactly one attribute, the one screen readers already read, and the visual state follows it.

Animating to auto height, still. A grid with one row track can transition 0fr to 1fr; the child needs min-h-0 overflow-hidden or it refuses to compress. Tailwind ships no grid-rows-[0fr] fraction utility, so both ends are arbitrary properties — correct, because a row track is not a design-scale value.

The keyboard layer is small on purpose. Arrow keys move through links and wrap, Home/End jump, Escape closes and refocuses the button, and opening moves focus to the first link. There is deliberately no focus trap: this is a disclosure, so Tab walks out naturally and a focusout listener closes it behind you. Compare demo 17, which is the modal flavour with inert and a real trap.

Plain links, no role=menu. The APG reserves role="menu" for application menus; on site navigation it makes screen readers promise app keyboard behaviour you have not built. A <nav> > <ul> of <a> elements is the recommendation, and it is what the markup does.

Make it yours

  • Delete the ArrowUp/ArrowDown block in the script and you still have a compliant disclosure — arrows are enhancement, Tab is the baseline.
  • Swap the slide-down for a fade by replacing the two [grid-template-rows:…] utilities with -translate-y-2 / translate-y-0; the ARIA layer is animation-agnostic.
  • Retheme the whole bar through --color-thb02-acc — focus rings, the current-page tint and the brand mark all read it.
  • Change the panel's easing with ease-thb02, defined once in @theme.
  • The <kbd> legend strip is demo furniture; delete that one <p> in production.

Gotchas — read before shipping

  • group-has-aria-expanded: needs the group class on the common ancestor of the button and the panel — here the bar, not the section.
  • The inner list needs min-h-0 overflow-hidden. Without them the 0fr track never compresses and the menu is stuck open.
  • Never role=&quot;menu&quot; or menuitem on site navigation; it changes how screen readers enumerate items and promises behaviour you have not implemented.
  • aria-expanded belongs on the button, not on the panel — it describes the state of what the control reveals, read at the control.
  • Closing on focusout needs a requestAnimationFrame before testing where focus went: blur fires before the next element focuses, and a naive check closes the menu while you tab inside it.
  • If you move focus into the panel on open you MUST return it to the button on Escape, or it is a WCAG 2.4.3 focus-order failure.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

:has() — which is what group-has-* compiles to — landed in Firefox 121, and the 0fr → 1fr row animation is Baseline 2023; Tailwind v4's own floor is higher than both. The ARIA pattern itself works in every browser with any screen reader.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…