28 Tailwind Hamburger Menus
09 / 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
Pure TailwindMIT licensed

Pure Tailwind Hamburger Menu Without JavaScript

The checkbox hack rebuilt for the :has() era, and this is where the port pays back demo 05's friction: group-has-checked: reads the state from an ANCESTOR, so the burger, the header, the fullscreen panel and even the page content in a different subtree all respond — no sibling order, no arbitrary variants, no hook classes. Same zero-JS guarantee, none of the DOM gymnastics.

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-09 w-full min-h-svh grid justify-center content-start bg-thb09-bg font-thb09-sans text-thb09-ink px-5 py-9" aria-label="Pure Tailwind :has() hamburger menu demo">
  <div class="group relative w-[min(400px,88vw)] h-135 rounded-3xl overflow-hidden bg-thb09-bg border border-thb09-edge">
    <input type="checkbox" id="thb09-t" class="peer absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">
    <div class="absolute inset-0 grid content-start gap-3.5 px-6 pt-21.5 pb-6 transition-[scale,filter,opacity] duration-500 ease-thb09 group-has-checked:scale-96 group-has-checked:blur-[2px] group-has-checked:opacity-55 motion-reduce:transition-none motion-reduce:group-has-checked:blur-none" aria-hidden="true">
      <div class="flex flex-col justify-end h-47.5 mb-1.5 p-4.5 rounded-2xl bg-linear-[150deg,oklch(0.9_0.035_70),oklch(0.84_0.06_45)]">
        <b class="font-thb09-mono text-[30px] font-bold tracking-[-0.02em]">:has()</b>
        <span class="text-[12.5px] text-thb09-mut">state, readable anywhere</span>
      </div>
      <div class="h-2.75 w-[82%] rounded-full bg-thb09-line"></div>
      <div class="h-2.75 w-[60%] rounded-full bg-thb09-line"></div>
      <div class="h-2.75 w-[73%] rounded-full bg-thb09-line"></div>
    </div>
    <nav aria-label="Site" class="absolute inset-0 z-2 flex flex-col justify-center px-8 bg-linear-[170deg,color-mix(in_oklch,var(--color-thb09-panel)_92%,white),var(--color-thb09-panel)_60%] opacity-0 scale-[1.04] invisible [transition:opacity_.4s,scale_.5s_var(--ease-thb09),visibility_0s_.5s] group-has-checked:opacity-100 group-has-checked:scale-100 group-has-checked:visible group-has-checked:[transition:opacity_.4s,scale_.5s_var(--ease-thb09),visibility_0s_0s]">
      <a href="#index" style="--i:0" class="group/l flex items-center justify-between py-4 px-0.5 rounded border-b border-[oklch(1_0_0/0.12)] text-[30px] font-bold tracking-[-0.02em] text-thb09-panelink no-underline opacity-0 translate-y-3.5 transition-[color,opacity,translate] duration-350 ease-thb09 hover:text-thb09-panhover focus-visible:text-thb09-panhover focus-visible:outline-2 focus-visible:outline-thb09-acc focus-visible:outline-offset-4 group-has-checked:opacity-100 group-has-checked:translate-y-0 group-has-checked:[transition-delay:calc(140ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><span>Index</span><span class="text-lg text-thb09-acc opacity-0 -translate-x-2 transition-[opacity,translate] duration-250 group-hover/l:opacity-100 group-hover/l:translate-x-0 group-focus-visible/l:opacity-100 group-focus-visible/l:translate-x-0" aria-hidden="true">&rarr;</span></a>
      <a href="#essays" style="--i:1" class="group/l flex items-center justify-between py-4 px-0.5 rounded border-b border-[oklch(1_0_0/0.12)] text-[30px] font-bold tracking-[-0.02em] text-thb09-panelink no-underline opacity-0 translate-y-3.5 transition-[color,opacity,translate] duration-350 ease-thb09 hover:text-thb09-panhover focus-visible:text-thb09-panhover focus-visible:outline-2 focus-visible:outline-thb09-acc focus-visible:outline-offset-4 group-has-checked:opacity-100 group-has-checked:translate-y-0 group-has-checked:[transition-delay:calc(140ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><span>Essays</span><span class="text-lg text-thb09-acc opacity-0 -translate-x-2 transition-[opacity,translate] duration-250 group-hover/l:opacity-100 group-hover/l:translate-x-0 group-focus-visible/l:opacity-100 group-focus-visible/l:translate-x-0" aria-hidden="true">&rarr;</span></a>
      <a href="#library" style="--i:2" class="group/l flex items-center justify-between py-4 px-0.5 rounded border-b border-[oklch(1_0_0/0.12)] text-[30px] font-bold tracking-[-0.02em] text-thb09-panelink no-underline opacity-0 translate-y-3.5 transition-[color,opacity,translate] duration-350 ease-thb09 hover:text-thb09-panhover focus-visible:text-thb09-panhover focus-visible:outline-2 focus-visible:outline-thb09-acc focus-visible:outline-offset-4 group-has-checked:opacity-100 group-has-checked:translate-y-0 group-has-checked:[transition-delay:calc(140ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><span>Library</span><span class="text-lg text-thb09-acc opacity-0 -translate-x-2 transition-[opacity,translate] duration-250 group-hover/l:opacity-100 group-hover/l:translate-x-0 group-focus-visible/l:opacity-100 group-focus-visible/l:translate-x-0" aria-hidden="true">&rarr;</span></a>
      <a href="#colophon" style="--i:3" class="group/l flex items-center justify-between py-4 px-0.5 rounded border-b border-[oklch(1_0_0/0.12)] text-[30px] font-bold tracking-[-0.02em] text-thb09-panelink no-underline opacity-0 translate-y-3.5 transition-[color,opacity,translate] duration-350 ease-thb09 hover:text-thb09-panhover focus-visible:text-thb09-panhover focus-visible:outline-2 focus-visible:outline-thb09-acc focus-visible:outline-offset-4 group-has-checked:opacity-100 group-has-checked:translate-y-0 group-has-checked:[transition-delay:calc(140ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><span>Colophon</span><span class="text-lg text-thb09-acc opacity-0 -translate-x-2 transition-[opacity,translate] duration-250 group-hover/l:opacity-100 group-hover/l:translate-x-0 group-focus-visible/l:opacity-100 group-focus-visible/l:translate-x-0" aria-hidden="true">&rarr;</span></a>
    </nav>
    <header class="absolute inset-x-0 top-0 z-3 flex items-center justify-between h-17.5 pl-6 pr-4">
      <span class="text-base font-bold italic tracking-[-0.01em] transition-[color] duration-350 group-has-checked:text-thb09-panelink">marginalia</span>
      <label for="thb09-t" class="grid place-items-center w-11.5 h-11.5 rounded-full bg-thb09-ink text-thb09-bg cursor-pointer select-none transition-[background,color,scale] duration-300 hover:scale-106 peer-focus-visible:outline-2 peer-focus-visible:outline-thb09-acc peer-focus-visible:outline-offset-3 group-has-checked:bg-thb09-panelink group-has-checked:text-thb09-panel motion-reduce:transition-none">
        <span class="relative block w-4.5 h-3" aria-hidden="true">
          <i class="absolute left-0 top-0 w-4.5 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 group-has-checked:translate-y-[5px] group-has-checked:rotate-45 motion-reduce:transition-none"></i>
          <i class="absolute left-0 top-[5px] w-4.5 h-0.5 rounded-sm bg-current transition-[opacity] duration-200 group-has-checked:opacity-0 motion-reduce:transition-none"></i>
          <i class="absolute left-0 top-2.5 w-4.5 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 group-has-checked:-translate-y-[5px] group-has-checked:-rotate-45 motion-reduce:transition-none"></i>
        </span>
      </label>
    </header>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-thb09-bg: #f7f4ec;
  --color-thb09-bg: oklch(0.965 0.008 85);
  --color-thb09-edge: #ddd7cb;
  --color-thb09-edge: oklch(0.87 0.015 80);
  --color-thb09-line: #e2ddd0;
  --color-thb09-line: oklch(0.89 0.012 80);
  --color-thb09-ink: #38322a;
  --color-thb09-ink: oklch(0.24 0.02 60);
  --color-thb09-mut: #787064;
  --color-thb09-mut: oklch(0.52 0.02 60);
  --color-thb09-panel: #3a2f26;
  --color-thb09-panel: oklch(0.24 0.03 55);
  --color-thb09-panelink: #f3efe6;
  --color-thb09-panelink: oklch(0.95 0.015 80);
  --color-thb09-panhover: #d8bb92;
  --color-thb09-panhover: oklch(0.85 0.09 60);
  --color-thb09-acc: #b4713f;
  --color-thb09-acc: oklch(0.58 0.16 45);
  --font-thb09-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb09-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb09: 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-09 {
  width: 100%;
  min-height: 100svh;
}

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-thb09-bg: #f7f4ec;
  --color-thb09-bg: oklch(0.965 0.008 85);
  --color-thb09-edge: #ddd7cb;
  --color-thb09-edge: oklch(0.87 0.015 80);
  --color-thb09-line: #e2ddd0;
  --color-thb09-line: oklch(0.89 0.012 80);
  --color-thb09-ink: #38322a;
  --color-thb09-ink: oklch(0.24 0.02 60);
  --color-thb09-mut: #787064;
  --color-thb09-mut: oklch(0.52 0.02 60);
  --color-thb09-panel: #3a2f26;
  --color-thb09-panel: oklch(0.24 0.03 55);
  --color-thb09-panelink: #f3efe6;
  --color-thb09-panelink: oklch(0.95 0.015 80);
  --color-thb09-panhover: #d8bb92;
  --color-thb09-panhover: oklch(0.85 0.09 60);
  --color-thb09-acc: #b4713f;
  --color-thb09-acc: oklch(0.58 0.16 45);
  --font-thb09-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb09-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb09: 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-09 {
  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.

  • This is the demo the port was waiting for. .root:has(#t:checked) .x becomes group-has-checked: on the element itself — nine separate :has() rules in the CSS version collapse into inline variants, and demo 05's arbitrary-variant escape hatch disappears entirely.
  • The page-blur proves the point: that element is in a different subtree from the input, so peer-* could never reach it. group-has-checked: does, with no restructuring.
  • Because state no longer depends on source order, the header can sit LAST in the markup for stacking purposes — a freedom demo 05's ~ engine does not have.
  • peer-focus-visible: and group-has-checked: coexist on the label: one reads the input as a sibling for focus, the other reads it through the ancestor for state. Two mechanisms, one element, no conflict.
  • The ::after arrow becomes a real span driven by group/l, so it can be aria-hidden and never doubles the link's announcement.
  • Still zero JavaScript — the js field is empty, and nothing in the interaction depends on it.
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: Pure Tailwind Hamburger Menu Without JavaScript
Source: https://codefronts.com/navigation/tailwind-hamburger-menus/pure-tailwind-hamburger-menu-without-javascript/

The checkbox hack rebuilt for the :has() era, and this is where the port pays back demo 05's friction: group-has-checked: reads the state from an ANCESTOR, so the burger, the header, the fullscreen panel and even the page content in a different subtree all respond &mdash; no sibling order, no arbitrary variants, no hook classes. Same zero-JS guarantee, none of the DOM gymnastics.
## HTML
```html
<section class="thb-09 w-full min-h-svh grid justify-center content-start bg-thb09-bg font-thb09-sans text-thb09-ink px-5 py-9" aria-label="Pure Tailwind :has() hamburger menu demo">
  <div class="group relative w-[min(400px,88vw)] h-135 rounded-3xl overflow-hidden bg-thb09-bg border border-thb09-edge">
    <input type="checkbox" id="thb09-t" class="peer absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">
    <div class="absolute inset-0 grid content-start gap-3.5 px-6 pt-21.5 pb-6 transition-[scale,filter,opacity] duration-500 ease-thb09 group-has-checked:scale-96 group-has-checked:blur-[2px] group-has-checked:opacity-55 motion-reduce:transition-none motion-reduce:group-has-checked:blur-none" aria-hidden="true">
      <div class="flex flex-col justify-end h-47.5 mb-1.5 p-4.5 rounded-2xl bg-linear-[150deg,oklch(0.9_0.035_70),oklch(0.84_0.06_45)]">
        <b class="font-thb09-mono text-[30px] font-bold tracking-[-0.02em]">:has()</b>
        <span class="text-[12.5px] text-thb09-mut">state, readable anywhere</span>
      </div>
      <div class="h-2.75 w-[82%] rounded-full bg-thb09-line"></div>
      <div class="h-2.75 w-[60%] rounded-full bg-thb09-line"></div>
      <div class="h-2.75 w-[73%] rounded-full bg-thb09-line"></div>
    </div>
    <nav aria-label="Site" class="absolute inset-0 z-2 flex flex-col justify-center px-8 bg-linear-[170deg,color-mix(in_oklch,var(--color-thb09-panel)_92%,white),var(--color-thb09-panel)_60%] opacity-0 scale-[1.04] invisible [transition:opacity_.4s,scale_.5s_var(--ease-thb09),visibility_0s_.5s] group-has-checked:opacity-100 group-has-checked:scale-100 group-has-checked:visible group-has-checked:[transition:opacity_.4s,scale_.5s_var(--ease-thb09),visibility_0s_0s]">
      <a href="#index" style="--i:0" class="group/l flex items-center justify-between py-4 px-0.5 rounded border-b border-[oklch(1_0_0/0.12)] text-[30px] font-bold tracking-[-0.02em] text-thb09-panelink no-underline opacity-0 translate-y-3.5 transition-[color,opacity,translate] duration-350 ease-thb09 hover:text-thb09-panhover focus-visible:text-thb09-panhover focus-visible:outline-2 focus-visible:outline-thb09-acc focus-visible:outline-offset-4 group-has-checked:opacity-100 group-has-checked:translate-y-0 group-has-checked:[transition-delay:calc(140ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><span>Index</span><span class="text-lg text-thb09-acc opacity-0 -translate-x-2 transition-[opacity,translate] duration-250 group-hover/l:opacity-100 group-hover/l:translate-x-0 group-focus-visible/l:opacity-100 group-focus-visible/l:translate-x-0" aria-hidden="true">&rarr;</span></a>
      <a href="#essays" style="--i:1" class="group/l flex items-center justify-between py-4 px-0.5 rounded border-b border-[oklch(1_0_0/0.12)] text-[30px] font-bold tracking-[-0.02em] text-thb09-panelink no-underline opacity-0 translate-y-3.5 transition-[color,opacity,translate] duration-350 ease-thb09 hover:text-thb09-panhover focus-visible:text-thb09-panhover focus-visible:outline-2 focus-visible:outline-thb09-acc focus-visible:outline-offset-4 group-has-checked:opacity-100 group-has-checked:translate-y-0 group-has-checked:[transition-delay:calc(140ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><span>Essays</span><span class="text-lg text-thb09-acc opacity-0 -translate-x-2 transition-[opacity,translate] duration-250 group-hover/l:opacity-100 group-hover/l:translate-x-0 group-focus-visible/l:opacity-100 group-focus-visible/l:translate-x-0" aria-hidden="true">&rarr;</span></a>
      <a href="#library" style="--i:2" class="group/l flex items-center justify-between py-4 px-0.5 rounded border-b border-[oklch(1_0_0/0.12)] text-[30px] font-bold tracking-[-0.02em] text-thb09-panelink no-underline opacity-0 translate-y-3.5 transition-[color,opacity,translate] duration-350 ease-thb09 hover:text-thb09-panhover focus-visible:text-thb09-panhover focus-visible:outline-2 focus-visible:outline-thb09-acc focus-visible:outline-offset-4 group-has-checked:opacity-100 group-has-checked:translate-y-0 group-has-checked:[transition-delay:calc(140ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><span>Library</span><span class="text-lg text-thb09-acc opacity-0 -translate-x-2 transition-[opacity,translate] duration-250 group-hover/l:opacity-100 group-hover/l:translate-x-0 group-focus-visible/l:opacity-100 group-focus-visible/l:translate-x-0" aria-hidden="true">&rarr;</span></a>
      <a href="#colophon" style="--i:3" class="group/l flex items-center justify-between py-4 px-0.5 rounded border-b border-[oklch(1_0_0/0.12)] text-[30px] font-bold tracking-[-0.02em] text-thb09-panelink no-underline opacity-0 translate-y-3.5 transition-[color,opacity,translate] duration-350 ease-thb09 hover:text-thb09-panhover focus-visible:text-thb09-panhover focus-visible:outline-2 focus-visible:outline-thb09-acc focus-visible:outline-offset-4 group-has-checked:opacity-100 group-has-checked:translate-y-0 group-has-checked:[transition-delay:calc(140ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><span>Colophon</span><span class="text-lg text-thb09-acc opacity-0 -translate-x-2 transition-[opacity,translate] duration-250 group-hover/l:opacity-100 group-hover/l:translate-x-0 group-focus-visible/l:opacity-100 group-focus-visible/l:translate-x-0" aria-hidden="true">&rarr;</span></a>
    </nav>
    <header class="absolute inset-x-0 top-0 z-3 flex items-center justify-between h-17.5 pl-6 pr-4">
      <span class="text-base font-bold italic tracking-[-0.01em] transition-[color] duration-350 group-has-checked:text-thb09-panelink">marginalia</span>
      <label for="thb09-t" class="grid place-items-center w-11.5 h-11.5 rounded-full bg-thb09-ink text-thb09-bg cursor-pointer select-none transition-[background,color,scale] duration-300 hover:scale-106 peer-focus-visible:outline-2 peer-focus-visible:outline-thb09-acc peer-focus-visible:outline-offset-3 group-has-checked:bg-thb09-panelink group-has-checked:text-thb09-panel motion-reduce:transition-none">
        <span class="relative block w-4.5 h-3" aria-hidden="true">
          <i class="absolute left-0 top-0 w-4.5 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 group-has-checked:translate-y-[5px] group-has-checked:rotate-45 motion-reduce:transition-none"></i>
          <i class="absolute left-0 top-[5px] w-4.5 h-0.5 rounded-sm bg-current transition-[opacity] duration-200 group-has-checked:opacity-0 motion-reduce:transition-none"></i>
          <i class="absolute left-0 top-2.5 w-4.5 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 group-has-checked:-translate-y-[5px] group-has-checked:-rotate-45 motion-reduce:transition-none"></i>
        </span>
      </label>
    </header>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-thb09-bg: #f7f4ec;
  --color-thb09-bg: oklch(0.965 0.008 85);
  --color-thb09-edge: #ddd7cb;
  --color-thb09-edge: oklch(0.87 0.015 80);
  --color-thb09-line: #e2ddd0;
  --color-thb09-line: oklch(0.89 0.012 80);
  --color-thb09-ink: #38322a;
  --color-thb09-ink: oklch(0.24 0.02 60);
  --color-thb09-mut: #787064;
  --color-thb09-mut: oklch(0.52 0.02 60);
  --color-thb09-panel: #3a2f26;
  --color-thb09-panel: oklch(0.24 0.03 55);
  --color-thb09-panelink: #f3efe6;
  --color-thb09-panelink: oklch(0.95 0.015 80);
  --color-thb09-panhover: #d8bb92;
  --color-thb09-panhover: oklch(0.85 0.09 60);
  --color-thb09-acc: #b4713f;
  --color-thb09-acc: oklch(0.58 0.16 45);
  --font-thb09-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb09-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb09: 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-09 {
  width: 100%;
  min-height: 100svh;
}
```

How this works

The variant that erases the sibling constraint. Demo 05's peer-checked: only reaches later siblings. group-has-checked: compiles to .group:has(:checked) &, so the condition lives on a component root and any descendant can key off it:

<div class="group relative">
  <input type="checkbox" id="thb09-t" class="peer absolute w-px h-px [clip-path:inset(50%)]">
  <div class="group-has-checked:scale-96 group-has-checked:blur-[2px]">page</div>
  <nav class="opacity-0 group-has-checked:opacity-100">…</nav>
  <header><label for="thb09-t">…</label></header>
</div>

The page <div> is not a sibling of anything relevant and it still responds. That is exactly what used to force people into JavaScript.

Note the ordering freedom. The header sits LAST in the markup here, after the panel, purely for stacking — something demo 05 cannot do, because its engine depends on source order. With :has() the DOM is free to be arranged for layering, semantics or reading order.

Keep the discipline. The input is clipped, not hidden, so it stays focusable and Space still toggles it; the label shows the ring via peer-focus-visible:. Those two lines are non-negotiable in any no-JS menu.

Same honest limits. It announces as a checkbox, Escape cannot close it, focus cannot be moved. What :has() buys is architectural freedom, not ARIA truth. For app navigation, demos 02 and 17 remain the answer.

Make it yours

  • Page-behind treatment is three utilities on one element (scale-96, blur-[2px], opacity-55). Delete them for a plain overlay, or add grayscale for editorial drama.
  • Panel motion: swap scale-[1.04] for translate-y-5 and it rises instead of settling. The :has() wiring is animation-agnostic.
  • Because state is root-readable, anything can join in — tint the brand, swap a logo colour, show a progress bar — by adding one group-has-checked: utility to that element.
  • Retheme through --color-thb09-panel and --color-thb09-acc.
  • Multiple menus on one page: keep each input id unique and each stage its own group. No hook classes to keep unique, unlike demo 05.

Gotchas — read before shipping

  • :has() invalidation is fast but not free. Keep group on a component root, never on html or body, so the browser re-checks a small subtree per toggle.
  • This pattern still cannot close on Escape or return focus — those are JS behaviours. If your audit requires them, and most app-nav audits do, graduate to demo 02 or 17.
  • group-has-checked: matches ANY checked descendant. With a form inside the same group, scope it — group-has-[#thb09-t:checked]: — or a stray checkbox opens your menu.
  • Keep the input focusable. Space is the ONLY keyboard affordance a no-JS menu has, and hidden removes it.
  • Firefox shipped :has() in 121. For enterprise ESR installs, demo 05's sibling engine is the fallback; ship both behind @supports selector(:has(*)) if you must.
  • Do not put group on the same element as peer-dependent utilities and expect both to read the input — they are different mechanisms with different targets.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

:has() is Chrome 105, Safari 15.4 and Firefox 121; everything else here is decade-old CSS. Tailwind v4's own floor is higher, so in practice the framework, not the selector, sets the baseline.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Arbitraryneeds bracket syntax

Search CodeFronts

Loading…