21 / 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 Mobile Bottom Navigation Bar with Floating Hamburger Toggle
The app-store staple: a four-tab bottom bar whose centre slot holds a raised floating hamburger sitting in a real curved notch cut from the bar with a CSS mask — not a fake shadow trick. Tapping it lifts a bottom sheet. The tabs are a radio group styled with has-checked:, which is the utility that makes one-of-N view switching work with no script at all.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="thb-21 w-full min-h-svh grid justify-center content-start bg-thb21-bg font-thb21-sans text-thb21-ink px-5 py-9" aria-label="Bottom navigation with floating hamburger demo">
<div class="group relative w-[min(400px,88vw)] h-140 rounded-3xl overflow-hidden bg-thb21-bg border border-thb21-edge">
<div class="grid gap-3.5 p-6" aria-hidden="true">
<div class="h-45 rounded-[18px] bg-linear-[145deg,oklch(0.9_0.03_290),oklch(0.85_0.05_265)]"></div>
<div class="h-2.75 w-[82%] rounded-full bg-thb21-line"></div>
<div class="h-2.75 w-[64%] rounded-full bg-thb21-line"></div>
<div class="h-2.75 w-[73%] rounded-full bg-thb21-line"></div>
</div>
<button type="button" tabindex="-1" aria-hidden="true" class="absolute inset-0 z-2 border-0 p-0 cursor-pointer opacity-0 invisible bg-[oklch(0.22_0.02_265/0.42)] [transition:opacity_.3s,visibility_0s_.3s] group-data-open:opacity-100 group-data-open:visible group-data-open:[transition:opacity_.3s,visibility_0s_0s]"></button>
<div id="thb21-sheet" aria-label="Quick actions" class="absolute inset-x-2.5 bottom-24 z-3 px-3.5 pt-2.5 pb-4 rounded-[22px] bg-thb21-panel border border-thb21-edge shadow-[0_-18px_44px_-22px_oklch(0.25_0.04_270/0.5)] translate-y-[calc(100%+110px)] invisible [transition:translate_.45s_var(--ease-thb21),visibility_0s_.45s] group-data-open:translate-y-0 group-data-open:visible group-data-open:[transition:translate_.45s_var(--ease-thb21),visibility_0s_0s] motion-reduce:[transition:opacity_.15s,visibility_0s_.15s]">
<i class="block w-9.5 h-[4.5px] mx-auto mt-0.5 mb-3 rounded-full bg-thb21-grab" aria-hidden="true"></i>
<div class="grid grid-cols-2 gap-2.5">
<a href="#new" style="--i:0" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>New entry
</a>
<a href="#scan" style="--i:1" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M4 8V5.5A1.5 1.5 0 0 1 5.5 4H8M16 4h2.5A1.5 1.5 0 0 1 20 5.5V8M20 16v2.5a1.5 1.5 0 0 1-1.5 1.5H16M8 20H5.5A1.5 1.5 0 0 1 4 18.5V16M4 12h16"/></svg>Scan
</a>
<a href="#send" style="--i:2" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="m4 11 16-7-7 16-2.5-6.5Z"/></svg>Send
</a>
<a href="#archive" style="--i:3" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><rect x="3.5" y="4" width="17" height="5" rx="1.5"/><path d="M5.5 9v9.5h13V9M10 13h4"/></svg>Archive
</a>
</div>
</div>
<div class="absolute inset-x-2.5 bottom-2.5 z-4 h-18.5">
<div class="absolute inset-0 drop-shadow-[0_14px_26px_oklch(0.3_0.03_265/0.28)]">
<nav aria-label="Tabs" class="absolute inset-0 flex items-stretch rounded-[22px] bg-thb21-panel [mask:radial-gradient(circle_37px_at_50%_0,transparent_98%,black_100%)]">
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" checked class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><path d="M3 11.5 12 4l9 7.5"/><path d="M5.5 10v10h13V10"/></svg>
<span class="text-[10px] font-semibold">Home</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
<span class="text-[10px] font-semibold">Search</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
<span class="flex-none w-18" aria-hidden="true"></span>
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><path d="M6 4h12v16l-6-4-6 4Z"/></svg>
<span class="text-[10px] font-semibold">Saved</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><circle cx="12" cy="8.5" r="3.5"/><path d="M5 20a7 7 0 0 1 14 0"/></svg>
<span class="text-[10px] font-semibold">Profile</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
</nav>
</div>
<button type="button" aria-expanded="false" aria-controls="thb21-sheet" aria-label="Open quick actions" class="absolute left-1/2 -top-6.5 -translate-x-1/2 grid place-items-center w-14 h-14 rounded-full border-0 bg-linear-[150deg,var(--color-thb21-acc),oklch(0.5_0.19_300)] text-white cursor-pointer shadow-[0_14px_28px_-10px_color-mix(in_oklch,var(--color-thb21-acc)_65%,transparent)] transition-[scale,box-shadow] duration-250 hover:scale-106 active:scale-93 focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-3 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-data-open:translate-y-[5px] group-data-open: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-data-open: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-data-open:-translate-y-[5px] group-data-open:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
</div>
</div>
</section><section class="thb-21 w-full min-h-svh grid justify-center content-start bg-thb21-bg font-thb21-sans text-thb21-ink px-5 py-9" aria-label="Bottom navigation with floating hamburger demo">
<div class="group relative w-[min(400px,88vw)] h-140 rounded-3xl overflow-hidden bg-thb21-bg border border-thb21-edge">
<div class="grid gap-3.5 p-6" aria-hidden="true">
<div class="h-45 rounded-[18px] bg-linear-[145deg,oklch(0.9_0.03_290),oklch(0.85_0.05_265)]"></div>
<div class="h-2.75 w-[82%] rounded-full bg-thb21-line"></div>
<div class="h-2.75 w-[64%] rounded-full bg-thb21-line"></div>
<div class="h-2.75 w-[73%] rounded-full bg-thb21-line"></div>
</div>
<button type="button" tabindex="-1" aria-hidden="true" class="absolute inset-0 z-2 border-0 p-0 cursor-pointer opacity-0 invisible bg-[oklch(0.22_0.02_265/0.42)] [transition:opacity_.3s,visibility_0s_.3s] group-data-open:opacity-100 group-data-open:visible group-data-open:[transition:opacity_.3s,visibility_0s_0s]"></button>
<div id="thb21-sheet" aria-label="Quick actions" class="absolute inset-x-2.5 bottom-24 z-3 px-3.5 pt-2.5 pb-4 rounded-[22px] bg-thb21-panel border border-thb21-edge shadow-[0_-18px_44px_-22px_oklch(0.25_0.04_270/0.5)] translate-y-[calc(100%+110px)] invisible [transition:translate_.45s_var(--ease-thb21),visibility_0s_.45s] group-data-open:translate-y-0 group-data-open:visible group-data-open:[transition:translate_.45s_var(--ease-thb21),visibility_0s_0s] motion-reduce:[transition:opacity_.15s,visibility_0s_.15s]">
<i class="block w-9.5 h-[4.5px] mx-auto mt-0.5 mb-3 rounded-full bg-thb21-grab" aria-hidden="true"></i>
<div class="grid grid-cols-2 gap-2.5">
<a href="#new" style="--i:0" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>New entry
</a>
<a href="#scan" style="--i:1" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M4 8V5.5A1.5 1.5 0 0 1 5.5 4H8M16 4h2.5A1.5 1.5 0 0 1 20 5.5V8M20 16v2.5a1.5 1.5 0 0 1-1.5 1.5H16M8 20H5.5A1.5 1.5 0 0 1 4 18.5V16M4 12h16"/></svg>Scan
</a>
<a href="#send" style="--i:2" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="m4 11 16-7-7 16-2.5-6.5Z"/></svg>Send
</a>
<a href="#archive" style="--i:3" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><rect x="3.5" y="4" width="17" height="5" rx="1.5"/><path d="M5.5 9v9.5h13V9M10 13h4"/></svg>Archive
</a>
</div>
</div>
<div class="absolute inset-x-2.5 bottom-2.5 z-4 h-18.5">
<div class="absolute inset-0 drop-shadow-[0_14px_26px_oklch(0.3_0.03_265/0.28)]">
<nav aria-label="Tabs" class="absolute inset-0 flex items-stretch rounded-[22px] bg-thb21-panel [mask:radial-gradient(circle_37px_at_50%_0,transparent_98%,black_100%)]">
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" checked class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><path d="M3 11.5 12 4l9 7.5"/><path d="M5.5 10v10h13V10"/></svg>
<span class="text-[10px] font-semibold">Home</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
<span class="text-[10px] font-semibold">Search</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
<span class="flex-none w-18" aria-hidden="true"></span>
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><path d="M6 4h12v16l-6-4-6 4Z"/></svg>
<span class="text-[10px] font-semibold">Saved</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><circle cx="12" cy="8.5" r="3.5"/><path d="M5 20a7 7 0 0 1 14 0"/></svg>
<span class="text-[10px] font-semibold">Profile</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
</nav>
</div>
<button type="button" aria-expanded="false" aria-controls="thb21-sheet" aria-label="Open quick actions" class="absolute left-1/2 -top-6.5 -translate-x-1/2 grid place-items-center w-14 h-14 rounded-full border-0 bg-linear-[150deg,var(--color-thb21-acc),oklch(0.5_0.19_300)] text-white cursor-pointer shadow-[0_14px_28px_-10px_color-mix(in_oklch,var(--color-thb21-acc)_65%,transparent)] transition-[scale,box-shadow] duration-250 hover:scale-106 active:scale-93 focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-3 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-data-open:translate-y-[5px] group-data-open: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-data-open: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-data-open:-translate-y-[5px] group-data-open:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-thb21-bg: #f3f4f8;
--color-thb21-bg: oklch(0.965 0.006 250);
--color-thb21-panel: #ffffff;
--color-thb21-panel: oklch(1 0 0);
--color-thb21-edge: #e3e4ea;
--color-thb21-edge: oklch(0.9 0.008 250);
--color-thb21-line: #e6e7ed;
--color-thb21-line: oklch(0.9 0.007 250);
--color-thb21-grab: #d9dae1;
--color-thb21-grab: oklch(0.87 0.008 250);
--color-thb21-ink: #34374a;
--color-thb21-ink: oklch(0.25 0.018 260);
--color-thb21-mut: #7a7d90;
--color-thb21-mut: oklch(0.56 0.014 260);
--color-thb21-acc: #6b4de8;
--color-thb21-acc: oklch(0.58 0.19 285);
--font-thb21-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb21-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb21: cubic-bezier(0.32, 0.72, 0.28, 1);
--ease-thb21in: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-thb21pop: cubic-bezier(0.3, 1.4, 0.4, 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-21 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-thb21-bg: #f3f4f8;
--color-thb21-bg: oklch(0.965 0.006 250);
--color-thb21-panel: #ffffff;
--color-thb21-panel: oklch(1 0 0);
--color-thb21-edge: #e3e4ea;
--color-thb21-edge: oklch(0.9 0.008 250);
--color-thb21-line: #e6e7ed;
--color-thb21-line: oklch(0.9 0.007 250);
--color-thb21-grab: #d9dae1;
--color-thb21-grab: oklch(0.87 0.008 250);
--color-thb21-ink: #34374a;
--color-thb21-ink: oklch(0.25 0.018 260);
--color-thb21-mut: #7a7d90;
--color-thb21-mut: oklch(0.56 0.014 260);
--color-thb21-acc: #6b4de8;
--color-thb21-acc: oklch(0.58 0.19 285);
--font-thb21-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb21-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb21: cubic-bezier(0.32, 0.72, 0.28, 1);
--ease-thb21in: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-thb21pop: cubic-bezier(0.3, 1.4, 0.4, 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-21 {
width: 100%;
min-height: 100svh;
}(function () {
document.querySelectorAll('.thb-21 > .group').forEach(function (stage) {
if (stage.dataset.bound) return; stage.dataset.bound = '1';
var fab = stage.querySelector('button[aria-controls="thb21-sheet"]'),
scrim = stage.querySelector('button[aria-hidden="true"]');
function set(open) {
stage.toggleAttribute('data-open', open);
fab.setAttribute('aria-expanded', String(open));
fab.setAttribute('aria-label', open ? 'Close quick actions' : 'Open quick actions');
}
fab.addEventListener('click', function () { set(fab.getAttribute('aria-expanded') !== 'true'); });
scrim.addEventListener('click', function () { set(false); });
stage.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && stage.hasAttribute('data-open')) { set(false); fab.focus(); }
});
});
})();(function () {
document.querySelectorAll('.thb-21 > .group').forEach(function (stage) {
if (stage.dataset.bound) return; stage.dataset.bound = '1';
var fab = stage.querySelector('button[aria-controls="thb21-sheet"]'),
scrim = stage.querySelector('button[aria-hidden="true"]');
function set(open) {
stage.toggleAttribute('data-open', open);
fab.setAttribute('aria-expanded', String(open));
fab.setAttribute('aria-label', open ? 'Close quick actions' : 'Open quick actions');
}
fab.addEventListener('click', function () { set(fab.getAttribute('aria-expanded') !== 'true'); });
scrim.addEventListener('click', function () { set(false); });
stage.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && stage.hasAttribute('data-open')) { set(false); fab.focus(); }
});
});
})();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-thb21-bg: #f3f4f8;
--color-thb21-bg: oklch(0.965 0.006 250);
--color-thb21-panel: #ffffff;
--color-thb21-panel: oklch(1 0 0);
--color-thb21-edge: #e3e4ea;
--color-thb21-edge: oklch(0.9 0.008 250);
--color-thb21-line: #e6e7ed;
--color-thb21-line: oklch(0.9 0.007 250);
--color-thb21-grab: #d9dae1;
--color-thb21-grab: oklch(0.87 0.008 250);
--color-thb21-ink: #34374a;
--color-thb21-ink: oklch(0.25 0.018 260);
--color-thb21-mut: #7a7d90;
--color-thb21-mut: oklch(0.56 0.014 260);
--color-thb21-acc: #6b4de8;
--color-thb21-acc: oklch(0.58 0.19 285);
--font-thb21-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb21-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb21: cubic-bezier(0.32, 0.72, 0.28, 1);
--ease-thb21in: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-thb21pop: cubic-bezier(0.3, 1.4, 0.4, 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-21 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-thb21-bg: #f3f4f8;
--color-thb21-bg: oklch(0.965 0.006 250);
--color-thb21-panel: #ffffff;
--color-thb21-panel: oklch(1 0 0);
--color-thb21-edge: #e3e4ea;
--color-thb21-edge: oklch(0.9 0.008 250);
--color-thb21-line: #e6e7ed;
--color-thb21-line: oklch(0.9 0.007 250);
--color-thb21-grab: #d9dae1;
--color-thb21-grab: oklch(0.87 0.008 250);
--color-thb21-ink: #34374a;
--color-thb21-ink: oklch(0.25 0.018 260);
--color-thb21-mut: #7a7d90;
--color-thb21-mut: oklch(0.56 0.014 260);
--color-thb21-acc: #6b4de8;
--color-thb21-acc: oklch(0.58 0.19 285);
--font-thb21-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb21-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb21: cubic-bezier(0.32, 0.72, 0.28, 1);
--ease-thb21in: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-thb21pop: cubic-bezier(0.3, 1.4, 0.4, 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-21 {
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.
.tab:has(:checked)becomeshas-checked:on the label, and:has(:focus-visible)becomeshas-focus-visible:. Four rules collapse into utilities on the element that owns the radio.- The notch survives as one arbitrary
[mask:radial-gradient(…)]value, and Tailwind emits the-webkit-pair, so the two hand-written mask declarations become one utility. filter: drop-shadowbecomes thedrop-shadow-[…]utility on the wrapper, and the reason for the wrapper (a mask clipsbox-shadow) is now visible in the DOM rather than explained in a comment.- The active tab's three effects (colour, icon lift, dot) each read
has-checked:from their own element, so styling the active state no longer means writing three descendant rules. - The sheet reuses the drawer engine with a vertical translate; the arbitrary
translate-y-[calc(100%+110px)]keeps the off-screen parking distance explicit. - The pop curve is an
@themetoken shared by the icon lift and the dot, which is what keeps the two micro-motions feeling like one gesture.
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 Mobile Bottom Navigation Bar with Floating Hamburger Toggle
Source: https://codefronts.com/navigation/tailwind-hamburger-menus/tailwind-mobile-bottom-navigation-bar-floating-hamburger-toggle/
The app-store staple: a four-tab bottom bar whose centre slot holds a raised floating hamburger sitting in a real curved notch cut from the bar with a CSS mask — not a fake shadow trick. Tapping it lifts a bottom sheet. The tabs are a radio group styled with has-checked:, which is the utility that makes one-of-N view switching work with no script at all.
## HTML
```html
<section class="thb-21 w-full min-h-svh grid justify-center content-start bg-thb21-bg font-thb21-sans text-thb21-ink px-5 py-9" aria-label="Bottom navigation with floating hamburger demo">
<div class="group relative w-[min(400px,88vw)] h-140 rounded-3xl overflow-hidden bg-thb21-bg border border-thb21-edge">
<div class="grid gap-3.5 p-6" aria-hidden="true">
<div class="h-45 rounded-[18px] bg-linear-[145deg,oklch(0.9_0.03_290),oklch(0.85_0.05_265)]"></div>
<div class="h-2.75 w-[82%] rounded-full bg-thb21-line"></div>
<div class="h-2.75 w-[64%] rounded-full bg-thb21-line"></div>
<div class="h-2.75 w-[73%] rounded-full bg-thb21-line"></div>
</div>
<button type="button" tabindex="-1" aria-hidden="true" class="absolute inset-0 z-2 border-0 p-0 cursor-pointer opacity-0 invisible bg-[oklch(0.22_0.02_265/0.42)] [transition:opacity_.3s,visibility_0s_.3s] group-data-open:opacity-100 group-data-open:visible group-data-open:[transition:opacity_.3s,visibility_0s_0s]"></button>
<div id="thb21-sheet" aria-label="Quick actions" class="absolute inset-x-2.5 bottom-24 z-3 px-3.5 pt-2.5 pb-4 rounded-[22px] bg-thb21-panel border border-thb21-edge shadow-[0_-18px_44px_-22px_oklch(0.25_0.04_270/0.5)] translate-y-[calc(100%+110px)] invisible [transition:translate_.45s_var(--ease-thb21),visibility_0s_.45s] group-data-open:translate-y-0 group-data-open:visible group-data-open:[transition:translate_.45s_var(--ease-thb21),visibility_0s_0s] motion-reduce:[transition:opacity_.15s,visibility_0s_.15s]">
<i class="block w-9.5 h-[4.5px] mx-auto mt-0.5 mb-3 rounded-full bg-thb21-grab" aria-hidden="true"></i>
<div class="grid grid-cols-2 gap-2.5">
<a href="#new" style="--i:0" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>New entry
</a>
<a href="#scan" style="--i:1" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M4 8V5.5A1.5 1.5 0 0 1 5.5 4H8M16 4h2.5A1.5 1.5 0 0 1 20 5.5V8M20 16v2.5a1.5 1.5 0 0 1-1.5 1.5H16M8 20H5.5A1.5 1.5 0 0 1 4 18.5V16M4 12h16"/></svg>Scan
</a>
<a href="#send" style="--i:2" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="m4 11 16-7-7 16-2.5-6.5Z"/></svg>Send
</a>
<a href="#archive" style="--i:3" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><rect x="3.5" y="4" width="17" height="5" rx="1.5"/><path d="M5.5 9v9.5h13V9M10 13h4"/></svg>Archive
</a>
</div>
</div>
<div class="absolute inset-x-2.5 bottom-2.5 z-4 h-18.5">
<div class="absolute inset-0 drop-shadow-[0_14px_26px_oklch(0.3_0.03_265/0.28)]">
<nav aria-label="Tabs" class="absolute inset-0 flex items-stretch rounded-[22px] bg-thb21-panel [mask:radial-gradient(circle_37px_at_50%_0,transparent_98%,black_100%)]">
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" checked class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><path d="M3 11.5 12 4l9 7.5"/><path d="M5.5 10v10h13V10"/></svg>
<span class="text-[10px] font-semibold">Home</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
<span class="text-[10px] font-semibold">Search</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
<span class="flex-none w-18" aria-hidden="true"></span>
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><path d="M6 4h12v16l-6-4-6 4Z"/></svg>
<span class="text-[10px] font-semibold">Saved</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><circle cx="12" cy="8.5" r="3.5"/><path d="M5 20a7 7 0 0 1 14 0"/></svg>
<span class="text-[10px] font-semibold">Profile</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
</nav>
</div>
<button type="button" aria-expanded="false" aria-controls="thb21-sheet" aria-label="Open quick actions" class="absolute left-1/2 -top-6.5 -translate-x-1/2 grid place-items-center w-14 h-14 rounded-full border-0 bg-linear-[150deg,var(--color-thb21-acc),oklch(0.5_0.19_300)] text-white cursor-pointer shadow-[0_14px_28px_-10px_color-mix(in_oklch,var(--color-thb21-acc)_65%,transparent)] transition-[scale,box-shadow] duration-250 hover:scale-106 active:scale-93 focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-3 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-data-open:translate-y-[5px] group-data-open: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-data-open: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-data-open:-translate-y-[5px] group-data-open:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-thb21-bg: #f3f4f8;
--color-thb21-bg: oklch(0.965 0.006 250);
--color-thb21-panel: #ffffff;
--color-thb21-panel: oklch(1 0 0);
--color-thb21-edge: #e3e4ea;
--color-thb21-edge: oklch(0.9 0.008 250);
--color-thb21-line: #e6e7ed;
--color-thb21-line: oklch(0.9 0.007 250);
--color-thb21-grab: #d9dae1;
--color-thb21-grab: oklch(0.87 0.008 250);
--color-thb21-ink: #34374a;
--color-thb21-ink: oklch(0.25 0.018 260);
--color-thb21-mut: #7a7d90;
--color-thb21-mut: oklch(0.56 0.014 260);
--color-thb21-acc: #6b4de8;
--color-thb21-acc: oklch(0.58 0.19 285);
--font-thb21-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb21-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb21: cubic-bezier(0.32, 0.72, 0.28, 1);
--ease-thb21in: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-thb21pop: cubic-bezier(0.3, 1.4, 0.4, 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-21 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(function () {
document.querySelectorAll('.thb-21 > .group').forEach(function (stage) {
if (stage.dataset.bound) return; stage.dataset.bound = '1';
var fab = stage.querySelector('button[aria-controls="thb21-sheet"]'),
scrim = stage.querySelector('button[aria-hidden="true"]');
function set(open) {
stage.toggleAttribute('data-open', open);
fab.setAttribute('aria-expanded', String(open));
fab.setAttribute('aria-label', open ? 'Close quick actions' : 'Open quick actions');
}
fab.addEventListener('click', function () { set(fab.getAttribute('aria-expanded') !== 'true'); });
scrim.addEventListener('click', function () { set(false); });
stage.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && stage.hasAttribute('data-open')) { set(false); fab.focus(); }
});
});
})();
```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 Mobile Bottom Navigation Bar with Floating Hamburger Toggle
Source: https://codefronts.com/navigation/tailwind-hamburger-menus/tailwind-mobile-bottom-navigation-bar-floating-hamburger-toggle/
The app-store staple: a four-tab bottom bar whose centre slot holds a raised floating hamburger sitting in a real curved notch cut from the bar with a CSS mask — not a fake shadow trick. Tapping it lifts a bottom sheet. The tabs are a radio group styled with has-checked:, which is the utility that makes one-of-N view switching work with no script at all.
## HTML
```html
<section class="thb-21 w-full min-h-svh grid justify-center content-start bg-thb21-bg font-thb21-sans text-thb21-ink px-5 py-9" aria-label="Bottom navigation with floating hamburger demo">
<div class="group relative w-[min(400px,88vw)] h-140 rounded-3xl overflow-hidden bg-thb21-bg border border-thb21-edge">
<div class="grid gap-3.5 p-6" aria-hidden="true">
<div class="h-45 rounded-[18px] bg-linear-[145deg,oklch(0.9_0.03_290),oklch(0.85_0.05_265)]"></div>
<div class="h-2.75 w-[82%] rounded-full bg-thb21-line"></div>
<div class="h-2.75 w-[64%] rounded-full bg-thb21-line"></div>
<div class="h-2.75 w-[73%] rounded-full bg-thb21-line"></div>
</div>
<button type="button" tabindex="-1" aria-hidden="true" class="absolute inset-0 z-2 border-0 p-0 cursor-pointer opacity-0 invisible bg-[oklch(0.22_0.02_265/0.42)] [transition:opacity_.3s,visibility_0s_.3s] group-data-open:opacity-100 group-data-open:visible group-data-open:[transition:opacity_.3s,visibility_0s_0s]"></button>
<div id="thb21-sheet" aria-label="Quick actions" class="absolute inset-x-2.5 bottom-24 z-3 px-3.5 pt-2.5 pb-4 rounded-[22px] bg-thb21-panel border border-thb21-edge shadow-[0_-18px_44px_-22px_oklch(0.25_0.04_270/0.5)] translate-y-[calc(100%+110px)] invisible [transition:translate_.45s_var(--ease-thb21),visibility_0s_.45s] group-data-open:translate-y-0 group-data-open:visible group-data-open:[transition:translate_.45s_var(--ease-thb21),visibility_0s_0s] motion-reduce:[transition:opacity_.15s,visibility_0s_.15s]">
<i class="block w-9.5 h-[4.5px] mx-auto mt-0.5 mb-3 rounded-full bg-thb21-grab" aria-hidden="true"></i>
<div class="grid grid-cols-2 gap-2.5">
<a href="#new" style="--i:0" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>New entry
</a>
<a href="#scan" style="--i:1" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M4 8V5.5A1.5 1.5 0 0 1 5.5 4H8M16 4h2.5A1.5 1.5 0 0 1 20 5.5V8M20 16v2.5a1.5 1.5 0 0 1-1.5 1.5H16M8 20H5.5A1.5 1.5 0 0 1 4 18.5V16M4 12h16"/></svg>Scan
</a>
<a href="#send" style="--i:2" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="m4 11 16-7-7 16-2.5-6.5Z"/></svg>Send
</a>
<a href="#archive" style="--i:3" class="flex items-center gap-2.75 min-h-14 px-4 rounded-[14px] border border-thb21-edge bg-thb21-bg text-[13.5px] font-semibold text-thb21-ink no-underline opacity-0 translate-y-3 transition-[border-color,color,opacity,translate] duration-350 ease-thb21in hover:border-thb21-acc hover:text-thb21-acc focus-visible:border-thb21-acc focus-visible:text-thb21-acc focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-2 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(100ms+var(--i)*40ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">
<svg viewBox="0 0 24 24" class="w-4.75 h-4.75 shrink-0 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><rect x="3.5" y="4" width="17" height="5" rx="1.5"/><path d="M5.5 9v9.5h13V9M10 13h4"/></svg>Archive
</a>
</div>
</div>
<div class="absolute inset-x-2.5 bottom-2.5 z-4 h-18.5">
<div class="absolute inset-0 drop-shadow-[0_14px_26px_oklch(0.3_0.03_265/0.28)]">
<nav aria-label="Tabs" class="absolute inset-0 flex items-stretch rounded-[22px] bg-thb21-panel [mask:radial-gradient(circle_37px_at_50%_0,transparent_98%,black_100%)]">
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" checked class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><path d="M3 11.5 12 4l9 7.5"/><path d="M5.5 10v10h13V10"/></svg>
<span class="text-[10px] font-semibold">Home</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
<span class="text-[10px] font-semibold">Search</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
<span class="flex-none w-18" aria-hidden="true"></span>
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><path d="M6 4h12v16l-6-4-6 4Z"/></svg>
<span class="text-[10px] font-semibold">Saved</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
<label class="relative flex flex-1 flex-col items-center justify-center gap-0.75 rounded-[14px] cursor-pointer text-thb21-mut transition-[color] duration-200 hover:text-thb21-ink has-checked:text-thb21-acc has-focus-visible:outline-2 has-focus-visible:outline-thb21-acc has-focus-visible:-outline-offset-4">
<input type="radio" name="thb21" class="absolute opacity-0 pointer-events-none">
<svg viewBox="0 0 24 24" class="w-5.25 h-5.25 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round] transition-[translate] duration-250 ease-thb21pop" aria-hidden="true"><circle cx="12" cy="8.5" r="3.5"/><path d="M5 20a7 7 0 0 1 14 0"/></svg>
<span class="text-[10px] font-semibold">Profile</span>
<i class="absolute bottom-1.75 w-1 h-1 rounded-full bg-thb21-acc scale-0 transition-[scale] duration-250 ease-thb21pop" aria-hidden="true"></i>
</label>
</nav>
</div>
<button type="button" aria-expanded="false" aria-controls="thb21-sheet" aria-label="Open quick actions" class="absolute left-1/2 -top-6.5 -translate-x-1/2 grid place-items-center w-14 h-14 rounded-full border-0 bg-linear-[150deg,var(--color-thb21-acc),oklch(0.5_0.19_300)] text-white cursor-pointer shadow-[0_14px_28px_-10px_color-mix(in_oklch,var(--color-thb21-acc)_65%,transparent)] transition-[scale,box-shadow] duration-250 hover:scale-106 active:scale-93 focus-visible:outline-2 focus-visible:outline-thb21-acc focus-visible:outline-offset-3 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-data-open:translate-y-[5px] group-data-open: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-data-open: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-data-open:-translate-y-[5px] group-data-open:-rotate-45 motion-reduce:transition-none"></i>
</span>
</button>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-thb21-bg: #f3f4f8;
--color-thb21-bg: oklch(0.965 0.006 250);
--color-thb21-panel: #ffffff;
--color-thb21-panel: oklch(1 0 0);
--color-thb21-edge: #e3e4ea;
--color-thb21-edge: oklch(0.9 0.008 250);
--color-thb21-line: #e6e7ed;
--color-thb21-line: oklch(0.9 0.007 250);
--color-thb21-grab: #d9dae1;
--color-thb21-grab: oklch(0.87 0.008 250);
--color-thb21-ink: #34374a;
--color-thb21-ink: oklch(0.25 0.018 260);
--color-thb21-mut: #7a7d90;
--color-thb21-mut: oklch(0.56 0.014 260);
--color-thb21-acc: #6b4de8;
--color-thb21-acc: oklch(0.58 0.19 285);
--font-thb21-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb21-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb21: cubic-bezier(0.32, 0.72, 0.28, 1);
--ease-thb21in: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-thb21pop: cubic-bezier(0.3, 1.4, 0.4, 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-21 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(function () {
document.querySelectorAll('.thb-21 > .group').forEach(function (stage) {
if (stage.dataset.bound) return; stage.dataset.bound = '1';
var fab = stage.querySelector('button[aria-controls="thb21-sheet"]'),
scrim = stage.querySelector('button[aria-hidden="true"]');
function set(open) {
stage.toggleAttribute('data-open', open);
fab.setAttribute('aria-expanded', String(open));
fab.setAttribute('aria-label', open ? 'Close quick actions' : 'Open quick actions');
}
fab.addEventListener('click', function () { set(fab.getAttribute('aria-expanded') !== 'true'); });
scrim.addEventListener('click', function () { set(false); });
stage.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && stage.hasAttribute('data-open')) { set(false); fab.focus(); }
});
});
})();
```How this works
The notch is the part people ask about. The bar's background is masked by a radial gradient that erases a disc-shaped bite at top centre:
class="[mask:radial-gradient(circle_37px_at_50%_0,transparent_98%,black_100%)]"One utility. The floating disc then centres itself over the bite and hovers 26px above the bar's top edge. Because the mask clips the BACKGROUND (and border, and shadow) but not layout, the bar's four real tabs simply leave the centre slot empty with a spacer.
Why the shadow moved to a wrapper. A mask clips box-shadow along with everything else, so elevation comes from drop-shadow-[0_14px_26px_…] on the parent: a drop-shadow filter traces the masked silhouette, notch included, while a box-shadow would draw the pre-mask rectangle.
Tabs are radios, and has-checked: is why that is now pleasant. Each label wraps a clipped <input type="radio">, and the label reads its own child's state:
class="text-thb21-mut has-checked:text-thb21-acc
has-focus-visible:outline-2"Screen readers announce “Home, radio button, 1 of 4, checked” for free. The disc is deliberately NOT in the radio group: it is a disclosure button with aria-expanded, because it opens something rather than switching a view.
Make it yours
- Notch fit: the mask radius (37px) should be the disc radius (28px) plus breathing room (9px). Move both together or the halo goes uneven.
- Sheet content is a plain grid — swap the shortcut tiles for a nav list or a share row; the shell (grabber, scrim, motion) does not change.
- Production: the dock becomes
fixedwithpb-[env(safe-area-inset-bottom)]. That is the one iOS line this pattern lives or dies by. - Hide-on-scroll pairs beautifully — translate the whole dock down when scrolling down; demo 25's scroll-driven timeline can drive it with no JS.
- Retheme through
--color-thb21-acc: the disc gradient, the active tab, the dot and every focus ring read it.
Gotchas — read before shipping
- A mask clips the bar's
box-shadow. Usedrop-shadow-*on a wrapper instead, or the elevation draws a rectangle straight across your notch. - The centre spacer must be inert space with no radio. Putting the disc in the radio group gives it toggle semantics it does not have — it OPENS something, it is not a view.
- Radio tab labels need BOTH icon and text. Icon-only labels with
aria-labelbreak the:checked-based context some assistive tech relies on; 10px text is fine, absent text is not. - The sheet's scroller needs
overscroll-contain, or a flick inside it scrolls the page behind — more noticeable at the bottom edge than anywhere else. [mask:…]is one arbitrary property; adding a second mask utility does not compose the waybackdrop-blur-*does. Write the whole mask value in one bracket.has-checked:matches any checked descendant. Keep one radio per label, or a nested control will light the wrong tab.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Gradient masks are unprefixed from Safari 15.4 and Chrome 120 (Tailwind emits the -webkit- pair for older Chromium), filter: drop-shadow and radios are ancient, and :has() — behind has-checked: — needs Firefox 121.