02 / 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 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.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
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 ·
<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 ·
<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">↓</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">↑</kbd> move ·
<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 ·
<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 & 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><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 ·
<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 ·
<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">↓</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">↑</kbd> move ·
<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 ·
<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 & 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;
}@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);
});
});
});
})();(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.
<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-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;
}@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;
}<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 clearest win in the collection:
:has([aria-expanded="true"])becomesgroup-has-aria-expanded:, and the icon morph becomesaria-expanded:on the button itself. No mirroreddata-openattribute exists, so the visual state cannot disagree with the accessibility state. grid-template-rows:0frhas 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 thea[aria-current="page"]rule, and the tint is an inlinecolor-mix()reading the accent token, so re-theming is still one line.- Six
@themecolour entries replace the CSS version's--tokensblock and buy the wholebg-/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
focusoutclose stay in JS. Keyboard behaviour is not a styling concern and has no variant form.
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 — 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 ·
<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 ·
<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">↓</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">↑</kbd> move ·
<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 ·
<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 & 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);
});
});
});
})();
```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 — 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 ·
<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 ·
<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">↓</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">↑</kbd> move ·
<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 ·
<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 & 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 thegroupclass 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 the0frtrack never compresses and the menu is stuck open. - Never
role="menu"ormenuitemon site navigation; it changes how screen readers enumerate items and promises behaviour you have not implemented. aria-expandedbelongs on the button, not on the panel — it describes the state of what the control reveals, read at the control.- Closing on
focusoutneeds arequestAnimationFramebefore 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
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 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.