28 Tailwind Hamburger Menus
01 / 28

All 28 in 28 Tailwind Hamburger Menus

  1. 01Tailwind Hamburger Menu with Clip-Path Circle Expansion
  2. 02Tailwind Keyboard Navigable Hamburger Menu with ARIA Attributes
  3. 03Tailwind Animated 3-Line to X Hamburger Icon Toggle
  4. 04Tailwind Slide-In Off-Canvas Drawer with Dimmed Backdrop Overlay
  5. 05Pure Tailwind Checkbox Hack Hamburger Navigation (No JavaScript)
  6. 06Tailwind Responsive Hamburger Menu with Integrated Search Bar & Social Links
  7. 07Tailwind Glassmorphism Backdrop Blur Hamburger Navigation
  8. 08Tailwind Push Page Navigation Menu (Off-Canvas Content Shift)
  9. 09Pure Tailwind Hamburger Menu Without JavaScript
  10. 10Tailwind Hamburger Menu Morphing to Close Icon Animation
  11. 11Tailwind Full Screen Mobile Navigation Overlay
  12. 12Tailwind Slide Out Sidebar Navigation / Off Canvas Menu
  13. 13Tailwind Responsive Navbar Mobile Hamburger Toggle Breakpoints
  14. 14Tailwind Minimalist Floating Hamburger Button
  15. 15Tailwind CSS Responsive Hamburger Menu Component
  16. 16Tailwind Rebuild of the Bootstrap 5 Navbar Hamburger Toggle
  17. 17Tailwind WCAG Accessible Hamburger Menu Example
  18. 18Pure Tailwind Dropdown Slide Down Mobile Hamburger Menu
  19. 19Simple 3-Line Tailwind Hamburger Icon Hover Effect
  20. 20Tailwind Full Screen Blur Overlay Hamburger Menu
  21. 21Tailwind Mobile Bottom Navigation Bar with Floating Hamburger Toggle
  22. 22Pure Tailwind Multi-Level Accordion Hamburger Dropdown Menu
  23. 23Flexbox Navbar Right-Aligned Tailwind Hamburger Icon
  24. 24Radial Ripple Expand Tailwind Hamburger Menu
  25. 25Sticky Tailwind Navbar Shrinking Hamburger Icon on Scroll
  26. 26Neumorphic Tailwind Hamburger Toggle Button
  27. 27Brutalist Design Bordered Tailwind Hamburger Menu
  28. 28Tailwind 3D Flip Canvas Mobile Hamburger Navigation
Tailwind + JSMIT licensed

Tailwind Hamburger Menu with Clip-Path Circle Expansion

The reveal every design-award site uses, ported to variants: tap the burger and the menu grows out of the button as a perfect circle, because the panel carries its closed and open clip-path as two arbitrary utilities on one element. The state is a data-open attribute on the stage, so the panel, the icon bars and the link stagger all read it through group-data-open: — the stylesheet keeps nothing but tokens.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="thb-01 w-full min-h-svh grid justify-center content-start bg-thb01-bg font-thb01-sans text-thb01-ink px-5 py-9" aria-label="Clip-path circle expansion hamburger menu demo">
  <div class="group relative w-[min(400px,88vw)] h-135 rounded-3xl overflow-hidden bg-thb01-bg border border-thb01-edge shadow-[0_30px_60px_-35px_oklch(0.3_0.05_285/0.45)]">
    <header class="relative z-1 flex items-center h-19 px-6">
      <span class="flex items-center gap-2.5 text-[15px] font-bold tracking-[-0.01em]">
        <i class="w-5.5 h-5.5 rounded-full bg-[conic-gradient(from_210deg,var(--color-thb01-acc),oklch(0.55_0.2_300),var(--color-thb01-acc))]" aria-hidden="true"></i>orbit.studio
      </span>
    </header>
    <div class="grid gap-3.5 px-6 pt-2" aria-hidden="true">
      <div class="h-47 mb-2 rounded-2xl bg-linear-[140deg,oklch(0.9_0.03_265),oklch(0.84_0.05_300)]"></div>
      <div class="h-3 w-[82%] rounded-full bg-thb01-line"></div>
      <div class="h-3 w-[64%] rounded-full bg-thb01-line"></div>
      <div class="h-3 w-[74%] rounded-full bg-thb01-line"></div>
      <div class="h-3 w-[46%] rounded-full bg-thb01-line"></div>
    </div>
    <nav id="thb01-menu" aria-label="Site" class="absolute inset-0 z-2 flex flex-col justify-center gap-1.5 px-8.5 bg-[radial-gradient(120%_100%_at_100%_0%,oklch(0.3_0.08_295),var(--color-thb01-menu)_55%)] invisible [clip-path:circle(0px_at_calc(100%-46px)_46px)] [transition:clip-path_.65s_var(--ease-thb01),visibility_0s_.65s] group-data-open:visible group-data-open:[clip-path:circle(150%_at_calc(100%-46px)_46px)] group-data-open:[transition:clip-path_.65s_var(--ease-thb01),visibility_0s_0s] motion-reduce:[clip-path:none] motion-reduce:opacity-0 motion-reduce:[transition:opacity_.15s,visibility_0s_.15s] motion-reduce:group-data-open:opacity-100 motion-reduce:group-data-open:[transition:opacity_.15s]">
      <a href="#work" style="--i:0" class="flex items-baseline gap-3.5 py-3 px-0.5 rounded-md text-[31px] font-bold tracking-[-0.02em] text-thb01-paper no-underline opacity-0 translate-y-4.5 transition-[opacity,translate,color] duration-400 ease-thb01 hover:text-thb01-acc focus-visible:text-thb01-acc focus-visible:outline-2 focus-visible:outline-thb01-acc focus-visible:outline-offset-4 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(180ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><small class="font-thb01-mono text-[11px] font-semibold text-thb01-acc tabular-nums">01</small>Work</a>
      <a href="#studio" style="--i:1" class="flex items-baseline gap-3.5 py-3 px-0.5 rounded-md text-[31px] font-bold tracking-[-0.02em] text-thb01-paper no-underline opacity-0 translate-y-4.5 transition-[opacity,translate,color] duration-400 ease-thb01 hover:text-thb01-acc focus-visible:text-thb01-acc focus-visible:outline-2 focus-visible:outline-thb01-acc focus-visible:outline-offset-4 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(180ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><small class="font-thb01-mono text-[11px] font-semibold text-thb01-acc tabular-nums">02</small>Studio</a>
      <a href="#journal" style="--i:2" class="flex items-baseline gap-3.5 py-3 px-0.5 rounded-md text-[31px] font-bold tracking-[-0.02em] text-thb01-paper no-underline opacity-0 translate-y-4.5 transition-[opacity,translate,color] duration-400 ease-thb01 hover:text-thb01-acc focus-visible:text-thb01-acc focus-visible:outline-2 focus-visible:outline-thb01-acc focus-visible:outline-offset-4 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(180ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><small class="font-thb01-mono text-[11px] font-semibold text-thb01-acc tabular-nums">03</small>Journal</a>
      <a href="#contact" style="--i:3" class="flex items-baseline gap-3.5 py-3 px-0.5 rounded-md text-[31px] font-bold tracking-[-0.02em] text-thb01-paper no-underline opacity-0 translate-y-4.5 transition-[opacity,translate,color] duration-400 ease-thb01 hover:text-thb01-acc focus-visible:text-thb01-acc focus-visible:outline-2 focus-visible:outline-thb01-acc focus-visible:outline-offset-4 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(180ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><small class="font-thb01-mono text-[11px] font-semibold text-thb01-acc tabular-nums">04</small>Contact</a>
      <p style="--i:4" class="mt-6.5 font-thb01-mono text-xs text-thb01-dim opacity-0 translate-y-4.5 transition-[opacity,translate] duration-400 ease-thb01 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(180ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">hello@orbit.studio &middot; NYC &mdash; LDN</p>
    </nav>
    <button type="button" aria-expanded="false" aria-controls="thb01-menu" aria-label="Open menu" class="absolute z-3 top-5.5 right-5.5 grid place-items-center w-12 h-12 rounded-full border-0 bg-thb01-ink cursor-pointer transition-[background,scale] duration-300 hover:scale-106 active:scale-94 focus-visible:outline-2 focus-visible:outline-thb01-acc focus-visible:outline-offset-3 group-data-open:bg-thb01-btnopen motion-reduce:transition-none">
      <span class="relative block w-5 h-3.5" aria-hidden="true">
        <i class="absolute left-0 top-0 w-5 h-0.5 rounded-sm bg-thb01-bg transition-[translate,rotate] duration-300 group-data-open:translate-y-1.5 group-data-open:rotate-45 motion-reduce:transition-none"></i>
        <i class="absolute left-0 top-1.5 w-5 h-0.5 rounded-sm bg-thb01-bg transition-[opacity,translate] duration-200 group-data-open:opacity-0 group-data-open:-translate-x-1.5 motion-reduce:transition-none"></i>
        <i class="absolute left-0 top-3 w-5 h-0.5 rounded-sm bg-thb01-bg transition-[translate,rotate] duration-300 group-data-open:-translate-y-1.5 group-data-open:-rotate-45 motion-reduce:transition-none"></i>
      </span>
    </button>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-thb01-bg: #f7f7fa;
  --color-thb01-bg: oklch(0.97 0.005 250);
  --color-thb01-ink: #2c2f3d;
  --color-thb01-ink: oklch(0.22 0.02 260);
  --color-thb01-edge: #dfe0e6;
  --color-thb01-edge: oklch(0.88 0.01 260);
  --color-thb01-line: #e4e4ea;
  --color-thb01-line: oklch(0.9 0.008 260);
  --color-thb01-menu: #2f2549;
  --color-thb01-menu: oklch(0.23 0.06 285);
  --color-thb01-btnopen: #453a63;
  --color-thb01-btnopen: oklch(0.32 0.07 295);
  --color-thb01-paper: #f6f5f0;
  --color-thb01-paper: oklch(0.96 0.01 90);
  --color-thb01-dim: #a49ab5;
  --color-thb01-dim: oklch(0.68 0.03 290);
  --color-thb01-acc: #e5b45c;
  --color-thb01-acc: oklch(0.8 0.16 85);
  --font-thb01-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb01-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb01: cubic-bezier(0.65, 0, 0.35, 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-01 {
  width: 100%;
  min-height: 100svh;
}
(function () {
  document.querySelectorAll('.thb-01 > .group').forEach(function (stage) {
    if (stage.dataset.bound) return; stage.dataset.bound = '1';
    var btn = stage.querySelector('button[aria-controls="thb01-menu"]');
    function set(open) {
      stage.toggleAttribute('data-open', open);
      btn.setAttribute('aria-expanded', String(open));
      btn.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
    }
    btn.addEventListener('click', function () { set(btn.getAttribute('aria-expanded') !== 'true'); });
    stage.addEventListener('keydown', function (e) {
      if (e.key === 'Escape' && stage.hasAttribute('data-open')) { set(false); btn.focus(); }
    });
  });
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-thb01-bg: #f7f7fa;
  --color-thb01-bg: oklch(0.97 0.005 250);
  --color-thb01-ink: #2c2f3d;
  --color-thb01-ink: oklch(0.22 0.02 260);
  --color-thb01-edge: #dfe0e6;
  --color-thb01-edge: oklch(0.88 0.01 260);
  --color-thb01-line: #e4e4ea;
  --color-thb01-line: oklch(0.9 0.008 260);
  --color-thb01-menu: #2f2549;
  --color-thb01-menu: oklch(0.23 0.06 285);
  --color-thb01-btnopen: #453a63;
  --color-thb01-btnopen: oklch(0.32 0.07 295);
  --color-thb01-paper: #f6f5f0;
  --color-thb01-paper: oklch(0.96 0.01 90);
  --color-thb01-dim: #a49ab5;
  --color-thb01-dim: oklch(0.68 0.03 290);
  --color-thb01-acc: #e5b45c;
  --color-thb01-acc: oklch(0.8 0.16 85);
  --font-thb01-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb01-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb01: cubic-bezier(0.65, 0, 0.35, 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-01 {
  width: 100%;
  min-height: 100svh;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • Eight [data-open] .x rules collapse into group on the stage plus group-data-open: on the panel, the three icon bars, the button background and the links. The open state is now readable from each element in isolation.
  • group-data-open: is the boolean attribute form — no brackets, no value. Reach for group-data-[state=open]: only when the attribute actually carries a value.
  • The delayed visibility needs the arbitrary [transition:...] shorthand: Tailwind's delay-* scale applies one delay to all transitioned properties, and this pattern needs 650ms on one and 0ms on the other.
  • Four :nth-child stagger rules become one utility reading --i from the element. Adding a fifth link means copying an <a> and incrementing a number.
  • The @media (prefers-reduced-motion) block becomes motion-reduce: variants inline — the circle is dropped with [clip-path:none] and the reveal becomes a 150ms fade.
  • The webfonts load as <link> tags in setup, not as an @import in the stylesheet: Tailwind emits user @import rules after its own @layer blocks, where the browser drops them.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Hamburger Menu with Clip-Path Circle Expansion
Source: https://codefronts.com/navigation/tailwind-hamburger-menus/tailwind-hamburger-menu-clip-path-circle-expansion/

The reveal every design-award site uses, ported to variants: tap the burger and the menu grows out of the button as a perfect circle, because the panel carries its closed and open clip-path as two arbitrary utilities on one element. The state is a data-open attribute on the stage, so the panel, the icon bars and the link stagger all read it through group-data-open: — the stylesheet keeps nothing but tokens.
## HTML
```html
<section class="thb-01 w-full min-h-svh grid justify-center content-start bg-thb01-bg font-thb01-sans text-thb01-ink px-5 py-9" aria-label="Clip-path circle expansion hamburger menu demo">
  <div class="group relative w-[min(400px,88vw)] h-135 rounded-3xl overflow-hidden bg-thb01-bg border border-thb01-edge shadow-[0_30px_60px_-35px_oklch(0.3_0.05_285/0.45)]">
    <header class="relative z-1 flex items-center h-19 px-6">
      <span class="flex items-center gap-2.5 text-[15px] font-bold tracking-[-0.01em]">
        <i class="w-5.5 h-5.5 rounded-full bg-[conic-gradient(from_210deg,var(--color-thb01-acc),oklch(0.55_0.2_300),var(--color-thb01-acc))]" aria-hidden="true"></i>orbit.studio
      </span>
    </header>
    <div class="grid gap-3.5 px-6 pt-2" aria-hidden="true">
      <div class="h-47 mb-2 rounded-2xl bg-linear-[140deg,oklch(0.9_0.03_265),oklch(0.84_0.05_300)]"></div>
      <div class="h-3 w-[82%] rounded-full bg-thb01-line"></div>
      <div class="h-3 w-[64%] rounded-full bg-thb01-line"></div>
      <div class="h-3 w-[74%] rounded-full bg-thb01-line"></div>
      <div class="h-3 w-[46%] rounded-full bg-thb01-line"></div>
    </div>
    <nav id="thb01-menu" aria-label="Site" class="absolute inset-0 z-2 flex flex-col justify-center gap-1.5 px-8.5 bg-[radial-gradient(120%_100%_at_100%_0%,oklch(0.3_0.08_295),var(--color-thb01-menu)_55%)] invisible [clip-path:circle(0px_at_calc(100%-46px)_46px)] [transition:clip-path_.65s_var(--ease-thb01),visibility_0s_.65s] group-data-open:visible group-data-open:[clip-path:circle(150%_at_calc(100%-46px)_46px)] group-data-open:[transition:clip-path_.65s_var(--ease-thb01),visibility_0s_0s] motion-reduce:[clip-path:none] motion-reduce:opacity-0 motion-reduce:[transition:opacity_.15s,visibility_0s_.15s] motion-reduce:group-data-open:opacity-100 motion-reduce:group-data-open:[transition:opacity_.15s]">
      <a href="#work" style="--i:0" class="flex items-baseline gap-3.5 py-3 px-0.5 rounded-md text-[31px] font-bold tracking-[-0.02em] text-thb01-paper no-underline opacity-0 translate-y-4.5 transition-[opacity,translate,color] duration-400 ease-thb01 hover:text-thb01-acc focus-visible:text-thb01-acc focus-visible:outline-2 focus-visible:outline-thb01-acc focus-visible:outline-offset-4 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(180ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><small class="font-thb01-mono text-[11px] font-semibold text-thb01-acc tabular-nums">01</small>Work</a>
      <a href="#studio" style="--i:1" class="flex items-baseline gap-3.5 py-3 px-0.5 rounded-md text-[31px] font-bold tracking-[-0.02em] text-thb01-paper no-underline opacity-0 translate-y-4.5 transition-[opacity,translate,color] duration-400 ease-thb01 hover:text-thb01-acc focus-visible:text-thb01-acc focus-visible:outline-2 focus-visible:outline-thb01-acc focus-visible:outline-offset-4 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(180ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><small class="font-thb01-mono text-[11px] font-semibold text-thb01-acc tabular-nums">02</small>Studio</a>
      <a href="#journal" style="--i:2" class="flex items-baseline gap-3.5 py-3 px-0.5 rounded-md text-[31px] font-bold tracking-[-0.02em] text-thb01-paper no-underline opacity-0 translate-y-4.5 transition-[opacity,translate,color] duration-400 ease-thb01 hover:text-thb01-acc focus-visible:text-thb01-acc focus-visible:outline-2 focus-visible:outline-thb01-acc focus-visible:outline-offset-4 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(180ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><small class="font-thb01-mono text-[11px] font-semibold text-thb01-acc tabular-nums">03</small>Journal</a>
      <a href="#contact" style="--i:3" class="flex items-baseline gap-3.5 py-3 px-0.5 rounded-md text-[31px] font-bold tracking-[-0.02em] text-thb01-paper no-underline opacity-0 translate-y-4.5 transition-[opacity,translate,color] duration-400 ease-thb01 hover:text-thb01-acc focus-visible:text-thb01-acc focus-visible:outline-2 focus-visible:outline-thb01-acc focus-visible:outline-offset-4 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(180ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150"><small class="font-thb01-mono text-[11px] font-semibold text-thb01-acc tabular-nums">04</small>Contact</a>
      <p style="--i:4" class="mt-6.5 font-thb01-mono text-xs text-thb01-dim opacity-0 translate-y-4.5 transition-[opacity,translate] duration-400 ease-thb01 group-data-open:opacity-100 group-data-open:translate-y-0 group-data-open:[transition-delay:calc(180ms+var(--i)*60ms)] motion-reduce:translate-y-0 motion-reduce:duration-150">hello@orbit.studio &middot; NYC &mdash; LDN</p>
    </nav>
    <button type="button" aria-expanded="false" aria-controls="thb01-menu" aria-label="Open menu" class="absolute z-3 top-5.5 right-5.5 grid place-items-center w-12 h-12 rounded-full border-0 bg-thb01-ink cursor-pointer transition-[background,scale] duration-300 hover:scale-106 active:scale-94 focus-visible:outline-2 focus-visible:outline-thb01-acc focus-visible:outline-offset-3 group-data-open:bg-thb01-btnopen motion-reduce:transition-none">
      <span class="relative block w-5 h-3.5" aria-hidden="true">
        <i class="absolute left-0 top-0 w-5 h-0.5 rounded-sm bg-thb01-bg transition-[translate,rotate] duration-300 group-data-open:translate-y-1.5 group-data-open:rotate-45 motion-reduce:transition-none"></i>
        <i class="absolute left-0 top-1.5 w-5 h-0.5 rounded-sm bg-thb01-bg transition-[opacity,translate] duration-200 group-data-open:opacity-0 group-data-open:-translate-x-1.5 motion-reduce:transition-none"></i>
        <i class="absolute left-0 top-3 w-5 h-0.5 rounded-sm bg-thb01-bg transition-[translate,rotate] duration-300 group-data-open:-translate-y-1.5 group-data-open:-rotate-45 motion-reduce:transition-none"></i>
      </span>
    </button>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-thb01-bg: #f7f7fa;
  --color-thb01-bg: oklch(0.97 0.005 250);
  --color-thb01-ink: #2c2f3d;
  --color-thb01-ink: oklch(0.22 0.02 260);
  --color-thb01-edge: #dfe0e6;
  --color-thb01-edge: oklch(0.88 0.01 260);
  --color-thb01-line: #e4e4ea;
  --color-thb01-line: oklch(0.9 0.008 260);
  --color-thb01-menu: #2f2549;
  --color-thb01-menu: oklch(0.23 0.06 285);
  --color-thb01-btnopen: #453a63;
  --color-thb01-btnopen: oklch(0.32 0.07 295);
  --color-thb01-paper: #f6f5f0;
  --color-thb01-paper: oklch(0.96 0.01 90);
  --color-thb01-dim: #a49ab5;
  --color-thb01-dim: oklch(0.68 0.03 290);
  --color-thb01-acc: #e5b45c;
  --color-thb01-acc: oklch(0.8 0.16 85);
  --font-thb01-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb01-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb01: cubic-bezier(0.65, 0, 0.35, 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-01 {
  width: 100%;
  min-height: 100svh;
}
```

## JavaScript
```js
(function () {
  document.querySelectorAll('.thb-01 > .group').forEach(function (stage) {
    if (stage.dataset.bound) return; stage.dataset.bound = '1';
    var btn = stage.querySelector('button[aria-controls="thb01-menu"]');
    function set(open) {
      stage.toggleAttribute('data-open', open);
      btn.setAttribute('aria-expanded', String(open));
      btn.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
    }
    btn.addEventListener('click', function () { set(btn.getAttribute('aria-expanded') !== 'true'); });
    stage.addEventListener('keydown', function (e) {
      if (e.key === 'Escape' && stage.hasAttribute('data-open')) { set(false); btn.focus(); }
    });
  });
})();
```

How this works

Both circles live on the panel. The CSS version writes the closed circle in one rule and the open circle in a [data-open] rule several screens away. In utilities the element holds both:

<nav class="absolute inset-0 invisible
  [clip-path:circle(0px_at_calc(100%-46px)_46px)]
  group-data-open:visible
  group-data-open:[clip-path:circle(150%_at_calc(100%-46px)_46px)]">

Move the anchor and you edit one element, not two rules — which matters, because a mismatched pair is the single most common bug in this pattern.

Why 150%. clip-path percentages resolve against a diagonal measure of the reference box, so 150% guarantees the circle swallows the farthest corner at any aspect ratio with no JS measuring.

Visibility rides along, late. A closed panel must be unfocusable, so invisible is on it — but flipped instantly it would pop the links away before the circle finishes collapsing. The arbitrary transition shorthand gives visibility a 650ms delay closing and none opening. This is the one place the shorthand beats Tailwind's transition-* scale, which cannot express per-property delays.

The script writes state; the markup writes style. Twelve lines flip data-open on the stage and keep aria-expanded on the button honest. Nothing in the script touches a class or a style, so restyling the open state never means reopening the JS.

Make it yours

  • Anchor point: edit the two calc(100%-46px)_46px pairs on the panel — 50%_100% blooms from a bottom-centre dock button.
  • Wavefront speed: --ease-thb01 and the .65s in the two arbitrary transitions. A springier feel: cubic-bezier(.3,1.36,.4,1) — clip-path tolerates overshoot because the circle simply grows past 100%.
  • Stagger tempo: the 60ms multiplier inside [transition-delay:calc(180ms+var(--i)*60ms)]; drop it to 0 and the links arrive with the circle.
  • Recolour through --color-thb01-menu and --color-thb01-acc in @theme — the panel gradient, the numerals and every focus ring read the same two tokens.
  • Promote to production by swapping the stage's relative for fixed inset-0 and deleting h-135; the pattern is viewport-ready as-is.

Gotchas — read before shipping

  • Arbitrary values cannot contain spaces. [clip-path:circle(0px_at_calc(100%-46px)_46px)] must use the underscore escape, and calc(100%-46px) must be typed closed-up.
  • Keep the button OUTSIDE the clipped panel — a sibling with a higher z-. Inside, it gets clipped away on close and focus falls nowhere.
  • Transition clip-path, never the panel's w-/h-. The clip runs on the compositor; animating size repaints layout every frame and wrecks INP.
  • invisible is load-bearing, not decoration. With opacity-0 alone a keyboard user tabs into a menu they cannot see — a WCAG 2.4.7 failure.
  • The class string must be statically visible to the compiler. A clip-path assembled at runtime from a template variable produces no CSS at all — keep varying values in a custom property.
  • The browser CDN build compiles in the page and is for prototyping only. Ship the CLI or a plugin build.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Animatable basic-shape clip-path has been Baseline since 2021, so the reveal itself runs far back; the floor quoted here is Tailwind v4's own generated sheet (cascade layers, @property, color-mix()).

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Arbitraryneeds bracket syntax

Search CodeFronts

Loading…