28 Tailwind Hamburger Menus
08 / 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 Push Page Navigation Menu (Off-Canvas Content Shift)

Instead of layering a drawer OVER the page, the whole canvas glides aside to reveal a menu resting beneath it — the page becomes a sheet you slide off a shelf. One element transitions; the menu never moves. In utilities the canvas's entire open state (push, 6% shrink, corner radius, deep shadow) is four group-data-open: classes sitting next to its resting state.

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-08 w-full min-h-svh grid justify-center content-start bg-thb08-bg font-thb08-sans text-thb08-ink px-5 py-9" aria-label="Push page off-canvas navigation demo">
  <div class="group relative w-[min(400px,88vw)] h-135 rounded-3xl overflow-hidden border border-thb08-edge bg-thb08-bg bg-[radial-gradient(120%_100%_at_0%_100%,oklch(0.26_0.06_300/0.6),transparent_55%)]" style="--w:270px">
    <nav id="thb08-menu" aria-label="Site" class="absolute inset-y-0 left-0 w-[var(--w)] flex flex-col gap-1 px-5.5 py-7.5 text-thb08-menuink">
      <b style="--i:0" class="mb-4.5 text-[17px] font-bold tracking-[-0.01em] -translate-x-6.5 opacity-40 transition-[translate,opacity] duration-500 ease-thb08 group-data-open:translate-x-0 group-data-open:opacity-100 group-data-open:[transition-delay:calc(var(--i)*35ms)] motion-reduce:translate-x-0 motion-reduce:duration-150">Good evening, Ash</b>
      <a href="#today" aria-current="page" style="--i:1" class="flex items-center gap-3 min-h-11.5 -mx-3 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb08-menumut -translate-x-6.5 opacity-40 transition-[translate,opacity,background,color] duration-500 ease-thb08 hover:bg-[oklch(1_0_0/0.07)] hover:text-thb08-menuink focus-visible:bg-[oklch(1_0_0/0.07)] focus-visible:text-thb08-menuink focus-visible:outline-2 focus-visible:outline-thb08-acc focus-visible:-outline-offset-2 aria-current:text-thb08-menuink aria-current:bg-[color-mix(in_oklch,var(--color-thb08-acc)_22%,transparent)] group-data-open:translate-x-0 group-data-open:opacity-100 group-data-open:[transition-delay:calc(var(--i)*35ms)] motion-reduce:translate-x-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.7] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2 2M19.1 4.9 17 7M7 17l-2.1 2.1"/></svg>Today
      </a>
      <a href="#inbox" style="--i:2" class="flex items-center gap-3 min-h-11.5 -mx-3 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb08-menumut -translate-x-6.5 opacity-40 transition-[translate,opacity,background,color] duration-500 ease-thb08 hover:bg-[oklch(1_0_0/0.07)] hover:text-thb08-menuink focus-visible:bg-[oklch(1_0_0/0.07)] focus-visible:text-thb08-menuink focus-visible:outline-2 focus-visible:outline-thb08-acc focus-visible:-outline-offset-2 group-data-open:translate-x-0 group-data-open:opacity-100 group-data-open:[transition-delay:calc(var(--i)*35ms)] motion-reduce:translate-x-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.7] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M3 13h5l2 3h4l2-3h5"/><path d="M5 5h14l2 8v6H3v-6Z"/></svg>Inbox
        <b class="ml-auto px-1.75 py-0.5 rounded-full bg-thb08-acc text-thb08-bg text-[10.5px] font-bold">7</b>
      </a>
      <a href="#projects" style="--i:3" class="flex items-center gap-3 min-h-11.5 -mx-3 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb08-menumut -translate-x-6.5 opacity-40 transition-[translate,opacity,background,color] duration-500 ease-thb08 hover:bg-[oklch(1_0_0/0.07)] hover:text-thb08-menuink focus-visible:bg-[oklch(1_0_0/0.07)] focus-visible:text-thb08-menuink focus-visible:outline-2 focus-visible:outline-thb08-acc focus-visible:-outline-offset-2 group-data-open:translate-x-0 group-data-open:opacity-100 group-data-open:[transition-delay:calc(var(--i)*35ms)] motion-reduce:translate-x-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.7] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><rect x="3.5" y="5" width="17" height="15" rx="2.5"/><path d="M3.5 10h17M9 5V3.5M15 5V3.5"/></svg>Projects
      </a>
      <a href="#notes" style="--i:4" class="flex items-center gap-3 min-h-11.5 -mx-3 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb08-menumut -translate-x-6.5 opacity-40 transition-[translate,opacity,background,color] duration-500 ease-thb08 hover:bg-[oklch(1_0_0/0.07)] hover:text-thb08-menuink focus-visible:bg-[oklch(1_0_0/0.07)] focus-visible:text-thb08-menuink focus-visible:outline-2 focus-visible:outline-thb08-acc focus-visible:-outline-offset-2 group-data-open:translate-x-0 group-data-open:opacity-100 group-data-open:[transition-delay:calc(var(--i)*35ms)] motion-reduce:translate-x-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.7] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M5 4h11l3 3v13H5Z"/><path d="M15.5 4v3.5H19M8.5 12h7M8.5 16h5"/></svg>Notes
      </a>
      <a href="#settings" style="--i:5" class="flex items-center gap-3 min-h-11.5 -mx-3 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb08-menumut -translate-x-6.5 opacity-40 transition-[translate,opacity,background,color] duration-500 ease-thb08 hover:bg-[oklch(1_0_0/0.07)] hover:text-thb08-menuink focus-visible:bg-[oklch(1_0_0/0.07)] focus-visible:text-thb08-menuink focus-visible:outline-2 focus-visible:outline-thb08-acc focus-visible:-outline-offset-2 group-data-open:translate-x-0 group-data-open:opacity-100 group-data-open:[transition-delay:calc(var(--i)*35ms)] motion-reduce:translate-x-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.7] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"/></svg>Settings
      </a>
      <span style="--i:6" class="mt-auto font-thb08-mono text-[11px] text-thb08-menudim -translate-x-6.5 opacity-40 transition-[translate,opacity] duration-500 ease-thb08 group-data-open:translate-x-0 group-data-open:opacity-100 group-data-open:[transition-delay:calc(var(--i)*35ms)] motion-reduce:translate-x-0 motion-reduce:duration-150">v4.2 &middot; push-canvas pattern</span>
    </nav>
    <div id="thb08-canvas" class="relative z-1 flex flex-col h-full bg-thb08-page transition-[translate,scale,border-radius,box-shadow] duration-500 ease-thb08 group-data-open:translate-x-[min(var(--w),76%)] group-data-open:scale-94 group-data-open:rounded-[22px] group-data-open:shadow-[-30px_24px_60px_-20px_oklch(0.12_0.04_280/0.6)] motion-reduce:duration-150">
      <header class="flex items-center gap-3 h-16 px-4 border-b border-thb08-edge2">
        <button type="button" aria-expanded="false" aria-controls="thb08-menu" aria-label="Open menu" class="grid place-items-center w-11 h-11 rounded-xl border-0 bg-transparent text-thb08-ink2 cursor-pointer transition-[background] duration-200 hover:bg-thb08-hover focus-visible:outline-2 focus-visible:outline-thb08-acc focus-visible:outline-offset-2">
          <span class="relative block w-4.75 h-3.25" aria-hidden="true">
            <i class="absolute left-0 top-0 w-4.75 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 group-data-open:translate-y-[5.5px] group-data-open:rotate-45 motion-reduce:transition-none"></i>
            <i class="absolute left-0 top-[5.5px] w-4.75 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-[11px] w-4.75 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 group-data-open:-translate-y-[5.5px] group-data-open:-rotate-45 motion-reduce:transition-none"></i>
          </span>
        </button>
        <strong class="text-[15px] font-bold tracking-[-0.01em] text-thb08-ink2">Daybook</strong>
        <span class="grid place-items-center ml-auto w-8 h-8 rounded-full bg-linear-[140deg,var(--color-thb08-acc),oklch(0.55_0.18_260)] text-white text-xs font-bold" aria-hidden="true">A</span>
      </header>
      <div class="flex-1 grid content-start gap-3.25 p-5" aria-hidden="true">
        <div class="h-37.5 rounded-2xl bg-linear-[140deg,oklch(0.93_0.02_290),oklch(0.88_0.045_310)]"></div>
        <div class="h-2.75 w-[83%] rounded-full bg-thb08-line"></div>
        <div class="h-2.75 w-[64%] rounded-full bg-thb08-line"></div>
        <div class="h-2.75 w-[71%] rounded-full bg-thb08-line"></div>
        <div class="flex gap-3">
          <span class="flex-1 h-21 rounded-[14px] bg-white border border-thb08-edge2"></span>
          <span class="flex-1 h-21 rounded-[14px] bg-white border border-thb08-edge2"></span>
        </div>
      </div>
    </div>
    <button type="button" tabindex="-1" aria-hidden="true" class="absolute inset-y-0 right-0 left-[var(--w)] z-5 hidden border-0 bg-transparent cursor-w-resize group-data-open:block"></button>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-thb08-bg: #2d2740;
  --color-thb08-bg: oklch(0.2 0.035 280);
  --color-thb08-edge: #494066;
  --color-thb08-edge: oklch(0.32 0.04 280);
  --color-thb08-menuink: #eae7f2;
  --color-thb08-menuink: oklch(0.93 0.01 280);
  --color-thb08-menumut: #b3adc4;
  --color-thb08-menumut: oklch(0.75 0.025 285);
  --color-thb08-menudim: #8b85a0;
  --color-thb08-menudim: oklch(0.6 0.03 285);
  --color-thb08-page: #fbfbfd;
  --color-thb08-page: oklch(0.98 0.004 260);
  --color-thb08-edge2: #e3e3ea;
  --color-thb08-edge2: oklch(0.9 0.008 260);
  --color-thb08-line: #e9e9ef;
  --color-thb08-line: oklch(0.92 0.006 265);
  --color-thb08-hover: #eeeef4;
  --color-thb08-hover: oklch(0.94 0.008 265);
  --color-thb08-ink: #f0eef7;
  --color-thb08-ink: oklch(0.95 0.01 280);
  --color-thb08-ink2: #34313f;
  --color-thb08-ink2: oklch(0.25 0.02 265);
  --color-thb08-acc: #b47ae4;
  --color-thb08-acc: oklch(0.7 0.16 300);
  --font-thb08-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb08-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb08: cubic-bezier(0.32, 0.72, 0.28, 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-08 {
  width: 100%;
  min-height: 100svh;
}
(function () {
  document.querySelectorAll('.thb-08 > .group').forEach(function (vp) {
    if (vp.dataset.bound) return; vp.dataset.bound = '1';
    var btn = vp.querySelector('button[aria-controls="thb08-menu"]'),
        canvas = vp.querySelector('#thb08-canvas'),
        catcher = vp.querySelector('button[aria-hidden="true"]');
    function set(open) {
      vp.toggleAttribute('data-open', open);
      btn.setAttribute('aria-expanded', String(open));
      if (open) { canvas.setAttribute('inert', ''); vp.querySelector('#thb08-menu a').focus(); }
      else { canvas.removeAttribute('inert'); btn.focus(); }
    }
    btn.addEventListener('click', function () { set(true); });
    catcher.addEventListener('click', function () { set(false); });
    vp.addEventListener('keydown', function (e) {
      if (e.key === 'Escape' && vp.hasAttribute('data-open')) set(false);
    });
  });
})();

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-thb08-bg: #2d2740;
  --color-thb08-bg: oklch(0.2 0.035 280);
  --color-thb08-edge: #494066;
  --color-thb08-edge: oklch(0.32 0.04 280);
  --color-thb08-menuink: #eae7f2;
  --color-thb08-menuink: oklch(0.93 0.01 280);
  --color-thb08-menumut: #b3adc4;
  --color-thb08-menumut: oklch(0.75 0.025 285);
  --color-thb08-menudim: #8b85a0;
  --color-thb08-menudim: oklch(0.6 0.03 285);
  --color-thb08-page: #fbfbfd;
  --color-thb08-page: oklch(0.98 0.004 260);
  --color-thb08-edge2: #e3e3ea;
  --color-thb08-edge2: oklch(0.9 0.008 260);
  --color-thb08-line: #e9e9ef;
  --color-thb08-line: oklch(0.92 0.006 265);
  --color-thb08-hover: #eeeef4;
  --color-thb08-hover: oklch(0.94 0.008 265);
  --color-thb08-ink: #f0eef7;
  --color-thb08-ink: oklch(0.95 0.01 280);
  --color-thb08-ink2: #34313f;
  --color-thb08-ink2: oklch(0.25 0.02 265);
  --color-thb08-acc: #b47ae4;
  --color-thb08-acc: oklch(0.7 0.16 300);
  --font-thb08-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb08-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb08: cubic-bezier(0.32, 0.72, 0.28, 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-08 {
  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.

  • The canvas's open state — push, lift, radius, shadow — is four group-data-open: utilities sitting immediately after its resting state, instead of a [data-open] .canvas block elsewhere in the sheet.
  • --w stays a plain inline custom property rather than an @theme entry, because per-instance geometry should not mint a utility. It is read by both w-[var(--w)] and translate-x-[min(var(--w),76%)].
  • The underlay parallax keeps its --i index and one [transition-delay:calc(var(--i)*35ms)] utility, replacing a :nth-child-free but still separate [data-open] rule.
  • The click-catcher's hidden / group-data-open:block pair replaces a display:none rule and its [data-open] override, and it stays a sibling of the canvas — outside the inert subtree, which is what keeps tap-to-close alive.
  • inert is still set by JS. There is no utility for it and there should not be: it is a semantic state, not a style.
  • Two colour namespaces (menu ink vs page ink) live in one @theme block, which is why the tokens carry menu/page in their names rather than relying on inheritance.
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 Push Page Navigation Menu (Off-Canvas Content Shift)
Source: https://codefronts.com/navigation/tailwind-hamburger-menus/tailwind-push-page-navigation-menu-off-canvas-content-shift/

Instead of layering a drawer OVER the page, the whole canvas glides aside to reveal a menu resting beneath it &mdash; the page becomes a sheet you slide off a shelf. One element transitions; the menu never moves. In utilities the canvas's entire open state (push, 6% shrink, corner radius, deep shadow) is four group-data-open: classes sitting next to its resting state.
## HTML
```html
<section class="thb-08 w-full min-h-svh grid justify-center content-start bg-thb08-bg font-thb08-sans text-thb08-ink px-5 py-9" aria-label="Push page off-canvas navigation demo">
  <div class="group relative w-[min(400px,88vw)] h-135 rounded-3xl overflow-hidden border border-thb08-edge bg-thb08-bg bg-[radial-gradient(120%_100%_at_0%_100%,oklch(0.26_0.06_300/0.6),transparent_55%)]" style="--w:270px">
    <nav id="thb08-menu" aria-label="Site" class="absolute inset-y-0 left-0 w-[var(--w)] flex flex-col gap-1 px-5.5 py-7.5 text-thb08-menuink">
      <b style="--i:0" class="mb-4.5 text-[17px] font-bold tracking-[-0.01em] -translate-x-6.5 opacity-40 transition-[translate,opacity] duration-500 ease-thb08 group-data-open:translate-x-0 group-data-open:opacity-100 group-data-open:[transition-delay:calc(var(--i)*35ms)] motion-reduce:translate-x-0 motion-reduce:duration-150">Good evening, Ash</b>
      <a href="#today" aria-current="page" style="--i:1" class="flex items-center gap-3 min-h-11.5 -mx-3 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb08-menumut -translate-x-6.5 opacity-40 transition-[translate,opacity,background,color] duration-500 ease-thb08 hover:bg-[oklch(1_0_0/0.07)] hover:text-thb08-menuink focus-visible:bg-[oklch(1_0_0/0.07)] focus-visible:text-thb08-menuink focus-visible:outline-2 focus-visible:outline-thb08-acc focus-visible:-outline-offset-2 aria-current:text-thb08-menuink aria-current:bg-[color-mix(in_oklch,var(--color-thb08-acc)_22%,transparent)] group-data-open:translate-x-0 group-data-open:opacity-100 group-data-open:[transition-delay:calc(var(--i)*35ms)] motion-reduce:translate-x-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.7] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2 2M19.1 4.9 17 7M7 17l-2.1 2.1"/></svg>Today
      </a>
      <a href="#inbox" style="--i:2" class="flex items-center gap-3 min-h-11.5 -mx-3 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb08-menumut -translate-x-6.5 opacity-40 transition-[translate,opacity,background,color] duration-500 ease-thb08 hover:bg-[oklch(1_0_0/0.07)] hover:text-thb08-menuink focus-visible:bg-[oklch(1_0_0/0.07)] focus-visible:text-thb08-menuink focus-visible:outline-2 focus-visible:outline-thb08-acc focus-visible:-outline-offset-2 group-data-open:translate-x-0 group-data-open:opacity-100 group-data-open:[transition-delay:calc(var(--i)*35ms)] motion-reduce:translate-x-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.7] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M3 13h5l2 3h4l2-3h5"/><path d="M5 5h14l2 8v6H3v-6Z"/></svg>Inbox
        <b class="ml-auto px-1.75 py-0.5 rounded-full bg-thb08-acc text-thb08-bg text-[10.5px] font-bold">7</b>
      </a>
      <a href="#projects" style="--i:3" class="flex items-center gap-3 min-h-11.5 -mx-3 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb08-menumut -translate-x-6.5 opacity-40 transition-[translate,opacity,background,color] duration-500 ease-thb08 hover:bg-[oklch(1_0_0/0.07)] hover:text-thb08-menuink focus-visible:bg-[oklch(1_0_0/0.07)] focus-visible:text-thb08-menuink focus-visible:outline-2 focus-visible:outline-thb08-acc focus-visible:-outline-offset-2 group-data-open:translate-x-0 group-data-open:opacity-100 group-data-open:[transition-delay:calc(var(--i)*35ms)] motion-reduce:translate-x-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.7] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><rect x="3.5" y="5" width="17" height="15" rx="2.5"/><path d="M3.5 10h17M9 5V3.5M15 5V3.5"/></svg>Projects
      </a>
      <a href="#notes" style="--i:4" class="flex items-center gap-3 min-h-11.5 -mx-3 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb08-menumut -translate-x-6.5 opacity-40 transition-[translate,opacity,background,color] duration-500 ease-thb08 hover:bg-[oklch(1_0_0/0.07)] hover:text-thb08-menuink focus-visible:bg-[oklch(1_0_0/0.07)] focus-visible:text-thb08-menuink focus-visible:outline-2 focus-visible:outline-thb08-acc focus-visible:-outline-offset-2 group-data-open:translate-x-0 group-data-open:opacity-100 group-data-open:[transition-delay:calc(var(--i)*35ms)] motion-reduce:translate-x-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.7] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M5 4h11l3 3v13H5Z"/><path d="M15.5 4v3.5H19M8.5 12h7M8.5 16h5"/></svg>Notes
      </a>
      <a href="#settings" style="--i:5" class="flex items-center gap-3 min-h-11.5 -mx-3 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb08-menumut -translate-x-6.5 opacity-40 transition-[translate,opacity,background,color] duration-500 ease-thb08 hover:bg-[oklch(1_0_0/0.07)] hover:text-thb08-menuink focus-visible:bg-[oklch(1_0_0/0.07)] focus-visible:text-thb08-menuink focus-visible:outline-2 focus-visible:outline-thb08-acc focus-visible:-outline-offset-2 group-data-open:translate-x-0 group-data-open:opacity-100 group-data-open:[transition-delay:calc(var(--i)*35ms)] motion-reduce:translate-x-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.7] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"/></svg>Settings
      </a>
      <span style="--i:6" class="mt-auto font-thb08-mono text-[11px] text-thb08-menudim -translate-x-6.5 opacity-40 transition-[translate,opacity] duration-500 ease-thb08 group-data-open:translate-x-0 group-data-open:opacity-100 group-data-open:[transition-delay:calc(var(--i)*35ms)] motion-reduce:translate-x-0 motion-reduce:duration-150">v4.2 &middot; push-canvas pattern</span>
    </nav>
    <div id="thb08-canvas" class="relative z-1 flex flex-col h-full bg-thb08-page transition-[translate,scale,border-radius,box-shadow] duration-500 ease-thb08 group-data-open:translate-x-[min(var(--w),76%)] group-data-open:scale-94 group-data-open:rounded-[22px] group-data-open:shadow-[-30px_24px_60px_-20px_oklch(0.12_0.04_280/0.6)] motion-reduce:duration-150">
      <header class="flex items-center gap-3 h-16 px-4 border-b border-thb08-edge2">
        <button type="button" aria-expanded="false" aria-controls="thb08-menu" aria-label="Open menu" class="grid place-items-center w-11 h-11 rounded-xl border-0 bg-transparent text-thb08-ink2 cursor-pointer transition-[background] duration-200 hover:bg-thb08-hover focus-visible:outline-2 focus-visible:outline-thb08-acc focus-visible:outline-offset-2">
          <span class="relative block w-4.75 h-3.25" aria-hidden="true">
            <i class="absolute left-0 top-0 w-4.75 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 group-data-open:translate-y-[5.5px] group-data-open:rotate-45 motion-reduce:transition-none"></i>
            <i class="absolute left-0 top-[5.5px] w-4.75 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-[11px] w-4.75 h-0.5 rounded-sm bg-current transition-[translate,rotate] duration-300 group-data-open:-translate-y-[5.5px] group-data-open:-rotate-45 motion-reduce:transition-none"></i>
          </span>
        </button>
        <strong class="text-[15px] font-bold tracking-[-0.01em] text-thb08-ink2">Daybook</strong>
        <span class="grid place-items-center ml-auto w-8 h-8 rounded-full bg-linear-[140deg,var(--color-thb08-acc),oklch(0.55_0.18_260)] text-white text-xs font-bold" aria-hidden="true">A</span>
      </header>
      <div class="flex-1 grid content-start gap-3.25 p-5" aria-hidden="true">
        <div class="h-37.5 rounded-2xl bg-linear-[140deg,oklch(0.93_0.02_290),oklch(0.88_0.045_310)]"></div>
        <div class="h-2.75 w-[83%] rounded-full bg-thb08-line"></div>
        <div class="h-2.75 w-[64%] rounded-full bg-thb08-line"></div>
        <div class="h-2.75 w-[71%] rounded-full bg-thb08-line"></div>
        <div class="flex gap-3">
          <span class="flex-1 h-21 rounded-[14px] bg-white border border-thb08-edge2"></span>
          <span class="flex-1 h-21 rounded-[14px] bg-white border border-thb08-edge2"></span>
        </div>
      </div>
    </div>
    <button type="button" tabindex="-1" aria-hidden="true" class="absolute inset-y-0 right-0 left-[var(--w)] z-5 hidden border-0 bg-transparent cursor-w-resize group-data-open:block"></button>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-thb08-bg: #2d2740;
  --color-thb08-bg: oklch(0.2 0.035 280);
  --color-thb08-edge: #494066;
  --color-thb08-edge: oklch(0.32 0.04 280);
  --color-thb08-menuink: #eae7f2;
  --color-thb08-menuink: oklch(0.93 0.01 280);
  --color-thb08-menumut: #b3adc4;
  --color-thb08-menumut: oklch(0.75 0.025 285);
  --color-thb08-menudim: #8b85a0;
  --color-thb08-menudim: oklch(0.6 0.03 285);
  --color-thb08-page: #fbfbfd;
  --color-thb08-page: oklch(0.98 0.004 260);
  --color-thb08-edge2: #e3e3ea;
  --color-thb08-edge2: oklch(0.9 0.008 260);
  --color-thb08-line: #e9e9ef;
  --color-thb08-line: oklch(0.92 0.006 265);
  --color-thb08-hover: #eeeef4;
  --color-thb08-hover: oklch(0.94 0.008 265);
  --color-thb08-ink: #f0eef7;
  --color-thb08-ink: oklch(0.95 0.01 280);
  --color-thb08-ink2: #34313f;
  --color-thb08-ink2: oklch(0.25 0.02 265);
  --color-thb08-acc: #b47ae4;
  --color-thb08-acc: oklch(0.7 0.16 300);
  --font-thb08-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb08-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb08: cubic-bezier(0.32, 0.72, 0.28, 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-08 {
  width: 100%;
  min-height: 100svh;
}
```

## JavaScript
```js
(function () {
  document.querySelectorAll('.thb-08 > .group').forEach(function (vp) {
    if (vp.dataset.bound) return; vp.dataset.bound = '1';
    var btn = vp.querySelector('button[aria-controls="thb08-menu"]'),
        canvas = vp.querySelector('#thb08-canvas'),
        catcher = vp.querySelector('button[aria-hidden="true"]');
    function set(open) {
      vp.toggleAttribute('data-open', open);
      btn.setAttribute('aria-expanded', String(open));
      if (open) { canvas.setAttribute('inert', ''); vp.querySelector('#thb08-menu a').focus(); }
      else { canvas.removeAttribute('inert'); btn.focus(); }
    }
    btn.addEventListener('click', function () { set(true); });
    catcher.addEventListener('click', function () { set(false); });
    vp.addEventListener('keydown', function (e) {
      if (e.key === 'Escape' && vp.hasAttribute('data-open')) set(false);
    });
  });
})();
```

How this works

Two layers, one transitioning element. The menu is a static underlay; the canvas is everything else:

<div class="group relative overflow-hidden" style="--w:270px">
  <nav class="absolute inset-y-0 left-0 w-[var(--w)]">…</nav>
  <div class="relative z-1 h-full transition-[translate,scale,border-radius,box-shadow]
    group-data-open:translate-x-[min(var(--w),76%)]
    group-data-open:scale-94 group-data-open:rounded-[22px]">…</div>
</div>

Because the menu never animates, its links cannot smear or judder — they are simply revealed.

The scale is doing UX work, not decoration. At scale-94 the user sees the page persists: this is a mode, not a navigation away. That is the psychology that made push menus a mobile-app staple, and it costs one utility.

Two accessibility details that are easy to get wrong. While open the script sets inert on the canvas, so its content drops out of the tab order in one attribute. The click-catcher that closes the menu therefore has to live OUTSIDE the canvas — an inert subtree swallows pointer events, so a catcher nested inside would be dead on arrival.

The push distance and the menu width are one number. --w is deliberately a plain custom property, not an @theme entry: it is per-instance geometry, not a design-scale value, and nothing should mint a w-w utility. min(var(--w),76%) keeps a grabbable page sliver on narrow screens.

Make it yours

  • Push distance is the --w inline custom property on the viewport. Change it once and the menu width follows.
  • Delete group-data-open:scale-94 and group-data-open:rounded-[22px] for the flat classic push.
  • Right-side push: move the menu to right-0 and negate the translate to -translate-x-[min(var(--w),76%)].
  • Parallax depth: the -translate-x-6.5 resting offset and the 35ms stagger multiplier on the menu items.
  • Production: the viewport keeps overflow-hidden; add touch-action-none to the click-catcher as your body-scroll-lock hook.

Gotchas — read before shipping

  • The viewport wrapper MUST keep overflow-hidden, or the pushed canvas widens the document and the browser grows a horizontal scrollbar mid-animation. Classic push-menu bug.
  • fixed children INSIDE the canvas (sticky headers, FABs) stop being viewport-fixed the moment the canvas transforms — a transformed element becomes their containing block. Keep fixed UI outside the canvas.
  • Never animate ml-* or left-* for the push. translate-x-* keeps it on the compositor.
  • Put the click-catcher outside the canvas. Inside an inert subtree it receives no pointer events and tap-to-close silently dies.
  • Without inert, keyboard users tab into a page they can barely see at 94% scale — a real focus-order failure, not a theoretical one.
  • scale-94 exists on the v4 scale, but any custom value needs the bracket form (scale-[0.935]) — and remember scale is a multiplier, not a percentage utility.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Transform transitions are universal; the inert attribute needs Chrome 102, Safari 15.5 and Firefox 112. The floor quoted here is Tailwind v4's own generated sheet.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Arbitraryneeds bracket syntax

Search CodeFronts

Loading…