09 / 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
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.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
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">→</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">→</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">→</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">→</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><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">→</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">→</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">→</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">→</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;
}@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.
<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-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;
}@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;
}<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.
- This is the demo the port was waiting for.
.root:has(#t:checked) .xbecomesgroup-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:andgroup-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
::afterarrow becomes a real span driven bygroup/l, so it can bearia-hiddenand never doubles the link's announcement. - Still zero JavaScript — the
jsfield is empty, and nothing in the interaction depends on it.
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 — 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">→</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">→</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">→</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">→</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;
}
```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 — 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">→</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">→</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">→</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">→</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 addgrayscalefor editorial drama. - Panel motion: swap
scale-[1.04]fortranslate-y-5and 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-paneland--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. Keepgroupon a component root, never onhtmlorbody, 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 samegroup, 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
hiddenremoves 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
groupon the same element aspeer-dependent utilities and expect both to read the input — they are different mechanisms with different targets.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 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.