28 Tailwind Hamburger Menus
04 / 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 Slide-In Off-Canvas Drawer with Dimmed Backdrop Overlay

The app-drawer everyone means when they say &ldquo;hamburger menu&rdquo;: a right-hand panel that slides in on translate-x (never on offsets), over a scrim that dims and blurs the page behind it. In utilities the drawer and the scrim each carry their own closed and open geometry, and the scrim stays a real <button> — so click-outside-to-close needs no document listener.

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-04 w-full min-h-svh grid justify-center content-start bg-thb04-bg font-thb04-sans text-thb04-ink px-5 py-9" aria-label="Off-canvas drawer with dimmed backdrop demo">
  <div class="group relative w-[min(400px,88vw)] h-135 rounded-3xl overflow-hidden bg-thb04-bg border border-thb04-edge shadow-[0_30px_60px_-35px_oklch(0.35_0.04_50/0.4)]">
    <header class="flex items-center justify-between h-17 pl-5.5 pr-3.5 bg-thb04-panel border-b border-thb04-edge">
      <span class="flex items-center gap-2.25 text-[15px] font-bold">
        <i class="w-4.75 h-4.75 rounded-md bg-[conic-gradient(from_200deg,var(--color-thb04-acc),oklch(0.75_0.12_85))]" aria-hidden="true"></i>Northwind
      </span>
      <button type="button" aria-expanded="false" aria-controls="thb04-drawer" aria-label="Open menu" class="group/btn grid place-items-center w-11 h-11 rounded-xl border-0 bg-transparent text-thb04-ink cursor-pointer transition-[background] duration-200 hover:bg-thb04-hover focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:outline-offset-2">
        <span class="relative block w-5 h-3.5" aria-hidden="true">
          <i class="absolute right-0 top-0 w-5 h-0.5 rounded-sm bg-current transition-[width] duration-250"></i>
          <i class="absolute right-0 top-1.5 w-3.5 h-0.5 rounded-sm bg-current transition-[width] duration-250 group-hover/btn:w-5"></i>
          <i class="absolute right-0 top-3 w-5 h-0.5 rounded-sm bg-current transition-[width] duration-250"></i>
        </span>
      </button>
    </header>
    <div class="grid gap-3.5 px-5.5 py-5" aria-hidden="true">
      <div class="h-37.5 rounded-2xl bg-linear-[140deg,oklch(0.92_0.03_60),oklch(0.88_0.05_35)]"></div>
      <div class="flex gap-3">
        <span class="flex-1 h-18 rounded-[13px] bg-thb04-panel border border-thb04-edge"></span>
        <span class="flex-1 h-18 rounded-[13px] bg-thb04-panel border border-thb04-edge"></span>
      </div>
      <div class="h-2.75 w-[88%] rounded-full bg-thb04-line"></div>
      <div class="h-2.75 w-[70%] rounded-full bg-thb04-line"></div>
      <div class="h-2.75 w-[79%] rounded-full bg-thb04-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.25_0.02_50/0.45)] backdrop-blur-[3px] [transition:opacity_.35s,visibility_0s_.35s] group-data-open:opacity-100 group-data-open:visible group-data-open:[transition:opacity_.35s,visibility_0s_0s]"></button>
    <aside id="thb04-drawer" aria-label="Menu" class="absolute inset-y-0 right-0 z-3 flex flex-col w-[min(84%,320px)] bg-thb04-panel shadow-[-24px_0_48px_-24px_oklch(0.25_0.03_50/0.5)] translate-x-full invisible [transition:translate_.4s_var(--ease-thb04),visibility_0s_.4s] group-data-open:translate-x-0 group-data-open:visible group-data-open:[transition:translate_.4s_var(--ease-thb04),visibility_0s_0s] motion-reduce:[transition:opacity_.15s,visibility_0s_.15s]">
      <header class="flex items-center justify-between pt-4.5 pb-3.5 pl-5.5 pr-3.5">
        <b class="text-base font-bold tracking-[-0.01em]">Menu</b>
        <button type="button" aria-label="Close menu" class="grid place-items-center w-10 h-10 rounded-[11px] border border-thb04-edge bg-thb04-panel text-thb04-ink cursor-pointer transition-[background,rotate] duration-300 hover:bg-thb04-hover hover:rotate-90 focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:outline-offset-2 motion-reduce:transition-none">
          <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg>
        </button>
      </header>
      <nav aria-label="Primary" class="flex flex-col gap-0.75 px-3 py-1.5 overflow-auto overscroll-contain">
        <a href="#home" aria-current="page" style="--i:0" class="flex items-center gap-3.25 min-h-12 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb04-mut opacity-0 translate-x-4 transition-[background,color,opacity,translate] duration-350 ease-thb04in hover:bg-thb04-hover hover:text-thb04-ink focus-visible:text-thb04-ink focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:-outline-offset-2 aria-current:text-thb04-acc aria-current:bg-[color-mix(in_oklch,var(--color-thb04-acc)_9%,transparent)] group-data-open:opacity-100 group-data-open:translate-x-0 group-data-open:[transition-delay:calc(120ms+var(--i)*45ms)] 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.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M3 11.5 12 4l9 7.5"/><path d="M5.5 10v10h13V10"/></svg>Home
        </a>
        <a href="#orders" style="--i:1" class="flex items-center gap-3.25 min-h-12 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb04-mut opacity-0 translate-x-4 transition-[background,color,opacity,translate] duration-350 ease-thb04in hover:bg-thb04-hover hover:text-thb04-ink focus-visible:text-thb04-ink focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:-outline-offset-2 group-data-open:opacity-100 group-data-open:translate-x-0 group-data-open:[transition-delay:calc(120ms+var(--i)*45ms)] 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.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M4.5 7h15L18 20H6Z"/><path d="M8.5 7a3.5 3.5 0 0 1 7 0"/></svg>Orders
          <b class="ml-auto px-2 py-0.5 rounded-full bg-thb04-acc text-white text-[11px] font-bold">3</b>
        </a>
        <a href="#saved" style="--i:2" class="flex items-center gap-3.25 min-h-12 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb04-mut opacity-0 translate-x-4 transition-[background,color,opacity,translate] duration-350 ease-thb04in hover:bg-thb04-hover hover:text-thb04-ink focus-visible:text-thb04-ink focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:-outline-offset-2 group-data-open:opacity-100 group-data-open:translate-x-0 group-data-open:[transition-delay:calc(120ms+var(--i)*45ms)] 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.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M12 20s-7.5-4.6-7.5-10A4.3 4.3 0 0 1 12 6.6 4.3 4.3 0 0 1 19.5 10c0 5.4-7.5 10-7.5 10Z"/></svg>Saved
        </a>
        <a href="#account" style="--i:3" class="flex items-center gap-3.25 min-h-12 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb04-mut opacity-0 translate-x-4 transition-[background,color,opacity,translate] duration-350 ease-thb04in hover:bg-thb04-hover hover:text-thb04-ink focus-visible:text-thb04-ink focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:-outline-offset-2 group-data-open:opacity-100 group-data-open:translate-x-0 group-data-open:[transition-delay:calc(120ms+var(--i)*45ms)] 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.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><circle cx="12" cy="8.5" r="3.5"/><path d="M5 20a7 7 0 0 1 14 0"/></svg>Account
        </a>
        <a href="#help" style="--i:4" class="flex items-center gap-3.25 min-h-12 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb04-mut opacity-0 translate-x-4 transition-[background,color,opacity,translate] duration-350 ease-thb04in hover:bg-thb04-hover hover:text-thb04-ink focus-visible:text-thb04-ink focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:-outline-offset-2 group-data-open:opacity-100 group-data-open:translate-x-0 group-data-open:[transition-delay:calc(120ms+var(--i)*45ms)] 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.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M9.6 9.4a2.5 2.5 0 1 1 3.4 3l-1 .8v1.3"/><path d="M12 17.5v.01"/></svg>Help centre
        </a>
      </nav>
      <footer class="mt-auto px-5.5 py-4 border-t border-thb04-edge">
        <button type="button" class="w-full min-h-11 rounded-xl border border-thb04-edge bg-thb04-bg font-thb04-sans text-[13.5px] font-semibold text-thb04-ink cursor-pointer transition-[border-color] duration-200 hover:border-thb04-edge2 focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:outline-offset-2">Sign out</button>
      </footer>
    </aside>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-thb04-bg: #fafaf7;
  --color-thb04-bg: oklch(0.98 0.004 80);
  --color-thb04-panel: #ffffff;
  --color-thb04-panel: oklch(1 0 0);
  --color-thb04-hover: #f2f1ec;
  --color-thb04-hover: oklch(0.955 0.007 80);
  --color-thb04-edge: #e5e3dc;
  --color-thb04-edge: oklch(0.9 0.008 80);
  --color-thb04-edge2: #b9b4a8;
  --color-thb04-edge2: oklch(0.75 0.02 60);
  --color-thb04-line: #e8e6df;
  --color-thb04-line: oklch(0.91 0.006 80);
  --color-thb04-ink: #3a3630;
  --color-thb04-ink: oklch(0.25 0.015 60);
  --color-thb04-mut: #7d7669;
  --color-thb04-mut: oklch(0.55 0.015 60);
  --color-thb04-acc: #c2643a;
  --color-thb04-acc: oklch(0.62 0.15 40);
  --font-thb04-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb04-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb04: cubic-bezier(0.32, 0.72, 0.28, 1);
  --ease-thb04in: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
   moment before the browser build has compiled its utilities. */

.thb-04 {
  width: 100%;
  min-height: 100svh;
}
(function () {
  document.querySelectorAll('.thb-04 > .group').forEach(function (stage) {
    if (stage.dataset.bound) return; stage.dataset.bound = '1';
    var btn = stage.querySelector('button[aria-controls="thb04-drawer"]'),
        scrim = stage.querySelector('button[aria-hidden="true"]'),
        close = stage.querySelector('button[aria-label="Close menu"]');
    function set(open) {
      stage.toggleAttribute('data-open', open);
      btn.setAttribute('aria-expanded', String(open));
      if (open) close.focus(); else btn.focus();
    }
    btn.addEventListener('click', function () { set(true); });
    scrim.addEventListener('click', function () { set(false); });
    close.addEventListener('click', function () { set(false); });
    stage.addEventListener('keydown', function (e) {
      if (e.key === 'Escape' && stage.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-thb04-bg: #fafaf7;
  --color-thb04-bg: oklch(0.98 0.004 80);
  --color-thb04-panel: #ffffff;
  --color-thb04-panel: oklch(1 0 0);
  --color-thb04-hover: #f2f1ec;
  --color-thb04-hover: oklch(0.955 0.007 80);
  --color-thb04-edge: #e5e3dc;
  --color-thb04-edge: oklch(0.9 0.008 80);
  --color-thb04-edge2: #b9b4a8;
  --color-thb04-edge2: oklch(0.75 0.02 60);
  --color-thb04-line: #e8e6df;
  --color-thb04-line: oklch(0.91 0.006 80);
  --color-thb04-ink: #3a3630;
  --color-thb04-ink: oklch(0.25 0.015 60);
  --color-thb04-mut: #7d7669;
  --color-thb04-mut: oklch(0.55 0.015 60);
  --color-thb04-acc: #c2643a;
  --color-thb04-acc: oklch(0.62 0.15 40);
  --font-thb04-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb04-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb04: cubic-bezier(0.32, 0.72, 0.28, 1);
  --ease-thb04in: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
   moment before the browser build has compiled its utilities. */

.thb-04 {
  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.

  • Two [data-open] .x rules become group-data-open: pairs on the scrim and the drawer, and the drawer's closed and open transforms now sit side by side in one class list.
  • translate-x-full / translate-x-0 is the whole slide. The utility name makes the compositor-safe choice shorter to type than the layout-thrashing one, which is the port's quiet performance win.
  • The scrim's material is composed inline — bg-[oklch(0.25_0.02_50/0.45)] backdrop-blur-[3px] — instead of a background rule plus a filter rule.
  • The five-link stagger drops the :nth-child map for style=&quot;--i:n&quot; read by one [transition-delay:calc(…)] utility, and the four-value transition-delay list from the CSS version is no longer needed at all.
  • overscroll-contain is a real utility (no brackets), which is the one iOS scroll-chaining fix that costs nothing to remember.
  • The icon's hover trick — the short middle bar growing to full width — uses a named group (group/btn) so it does not collide with the stage's own group.
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 Slide-In Off-Canvas Drawer with Dimmed Backdrop Overlay
Source: https://codefronts.com/navigation/tailwind-hamburger-menus/tailwind-slide-in-off-canvas-drawer-dimmed-backdrop-overlay/

The app-drawer everyone means when they say &ldquo;hamburger menu&rdquo;: a right-hand panel that slides in on translate-x (never on offsets), over a scrim that dims and blurs the page behind it. In utilities the drawer and the scrim each carry their own closed and open geometry, and the scrim stays a real <button> &mdash; so click-outside-to-close needs no document listener.
## HTML
```html
<section class="thb-04 w-full min-h-svh grid justify-center content-start bg-thb04-bg font-thb04-sans text-thb04-ink px-5 py-9" aria-label="Off-canvas drawer with dimmed backdrop demo">
  <div class="group relative w-[min(400px,88vw)] h-135 rounded-3xl overflow-hidden bg-thb04-bg border border-thb04-edge shadow-[0_30px_60px_-35px_oklch(0.35_0.04_50/0.4)]">
    <header class="flex items-center justify-between h-17 pl-5.5 pr-3.5 bg-thb04-panel border-b border-thb04-edge">
      <span class="flex items-center gap-2.25 text-[15px] font-bold">
        <i class="w-4.75 h-4.75 rounded-md bg-[conic-gradient(from_200deg,var(--color-thb04-acc),oklch(0.75_0.12_85))]" aria-hidden="true"></i>Northwind
      </span>
      <button type="button" aria-expanded="false" aria-controls="thb04-drawer" aria-label="Open menu" class="group/btn grid place-items-center w-11 h-11 rounded-xl border-0 bg-transparent text-thb04-ink cursor-pointer transition-[background] duration-200 hover:bg-thb04-hover focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:outline-offset-2">
        <span class="relative block w-5 h-3.5" aria-hidden="true">
          <i class="absolute right-0 top-0 w-5 h-0.5 rounded-sm bg-current transition-[width] duration-250"></i>
          <i class="absolute right-0 top-1.5 w-3.5 h-0.5 rounded-sm bg-current transition-[width] duration-250 group-hover/btn:w-5"></i>
          <i class="absolute right-0 top-3 w-5 h-0.5 rounded-sm bg-current transition-[width] duration-250"></i>
        </span>
      </button>
    </header>
    <div class="grid gap-3.5 px-5.5 py-5" aria-hidden="true">
      <div class="h-37.5 rounded-2xl bg-linear-[140deg,oklch(0.92_0.03_60),oklch(0.88_0.05_35)]"></div>
      <div class="flex gap-3">
        <span class="flex-1 h-18 rounded-[13px] bg-thb04-panel border border-thb04-edge"></span>
        <span class="flex-1 h-18 rounded-[13px] bg-thb04-panel border border-thb04-edge"></span>
      </div>
      <div class="h-2.75 w-[88%] rounded-full bg-thb04-line"></div>
      <div class="h-2.75 w-[70%] rounded-full bg-thb04-line"></div>
      <div class="h-2.75 w-[79%] rounded-full bg-thb04-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.25_0.02_50/0.45)] backdrop-blur-[3px] [transition:opacity_.35s,visibility_0s_.35s] group-data-open:opacity-100 group-data-open:visible group-data-open:[transition:opacity_.35s,visibility_0s_0s]"></button>
    <aside id="thb04-drawer" aria-label="Menu" class="absolute inset-y-0 right-0 z-3 flex flex-col w-[min(84%,320px)] bg-thb04-panel shadow-[-24px_0_48px_-24px_oklch(0.25_0.03_50/0.5)] translate-x-full invisible [transition:translate_.4s_var(--ease-thb04),visibility_0s_.4s] group-data-open:translate-x-0 group-data-open:visible group-data-open:[transition:translate_.4s_var(--ease-thb04),visibility_0s_0s] motion-reduce:[transition:opacity_.15s,visibility_0s_.15s]">
      <header class="flex items-center justify-between pt-4.5 pb-3.5 pl-5.5 pr-3.5">
        <b class="text-base font-bold tracking-[-0.01em]">Menu</b>
        <button type="button" aria-label="Close menu" class="grid place-items-center w-10 h-10 rounded-[11px] border border-thb04-edge bg-thb04-panel text-thb04-ink cursor-pointer transition-[background,rotate] duration-300 hover:bg-thb04-hover hover:rotate-90 focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:outline-offset-2 motion-reduce:transition-none">
          <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg>
        </button>
      </header>
      <nav aria-label="Primary" class="flex flex-col gap-0.75 px-3 py-1.5 overflow-auto overscroll-contain">
        <a href="#home" aria-current="page" style="--i:0" class="flex items-center gap-3.25 min-h-12 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb04-mut opacity-0 translate-x-4 transition-[background,color,opacity,translate] duration-350 ease-thb04in hover:bg-thb04-hover hover:text-thb04-ink focus-visible:text-thb04-ink focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:-outline-offset-2 aria-current:text-thb04-acc aria-current:bg-[color-mix(in_oklch,var(--color-thb04-acc)_9%,transparent)] group-data-open:opacity-100 group-data-open:translate-x-0 group-data-open:[transition-delay:calc(120ms+var(--i)*45ms)] 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.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M3 11.5 12 4l9 7.5"/><path d="M5.5 10v10h13V10"/></svg>Home
        </a>
        <a href="#orders" style="--i:1" class="flex items-center gap-3.25 min-h-12 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb04-mut opacity-0 translate-x-4 transition-[background,color,opacity,translate] duration-350 ease-thb04in hover:bg-thb04-hover hover:text-thb04-ink focus-visible:text-thb04-ink focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:-outline-offset-2 group-data-open:opacity-100 group-data-open:translate-x-0 group-data-open:[transition-delay:calc(120ms+var(--i)*45ms)] 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.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M4.5 7h15L18 20H6Z"/><path d="M8.5 7a3.5 3.5 0 0 1 7 0"/></svg>Orders
          <b class="ml-auto px-2 py-0.5 rounded-full bg-thb04-acc text-white text-[11px] font-bold">3</b>
        </a>
        <a href="#saved" style="--i:2" class="flex items-center gap-3.25 min-h-12 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb04-mut opacity-0 translate-x-4 transition-[background,color,opacity,translate] duration-350 ease-thb04in hover:bg-thb04-hover hover:text-thb04-ink focus-visible:text-thb04-ink focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:-outline-offset-2 group-data-open:opacity-100 group-data-open:translate-x-0 group-data-open:[transition-delay:calc(120ms+var(--i)*45ms)] 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.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M12 20s-7.5-4.6-7.5-10A4.3 4.3 0 0 1 12 6.6 4.3 4.3 0 0 1 19.5 10c0 5.4-7.5 10-7.5 10Z"/></svg>Saved
        </a>
        <a href="#account" style="--i:3" class="flex items-center gap-3.25 min-h-12 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb04-mut opacity-0 translate-x-4 transition-[background,color,opacity,translate] duration-350 ease-thb04in hover:bg-thb04-hover hover:text-thb04-ink focus-visible:text-thb04-ink focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:-outline-offset-2 group-data-open:opacity-100 group-data-open:translate-x-0 group-data-open:[transition-delay:calc(120ms+var(--i)*45ms)] 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.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><circle cx="12" cy="8.5" r="3.5"/><path d="M5 20a7 7 0 0 1 14 0"/></svg>Account
        </a>
        <a href="#help" style="--i:4" class="flex items-center gap-3.25 min-h-12 px-3 rounded-xl no-underline text-[14.5px] font-semibold text-thb04-mut opacity-0 translate-x-4 transition-[background,color,opacity,translate] duration-350 ease-thb04in hover:bg-thb04-hover hover:text-thb04-ink focus-visible:text-thb04-ink focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:-outline-offset-2 group-data-open:opacity-100 group-data-open:translate-x-0 group-data-open:[transition-delay:calc(120ms+var(--i)*45ms)] 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.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M9.6 9.4a2.5 2.5 0 1 1 3.4 3l-1 .8v1.3"/><path d="M12 17.5v.01"/></svg>Help centre
        </a>
      </nav>
      <footer class="mt-auto px-5.5 py-4 border-t border-thb04-edge">
        <button type="button" class="w-full min-h-11 rounded-xl border border-thb04-edge bg-thb04-bg font-thb04-sans text-[13.5px] font-semibold text-thb04-ink cursor-pointer transition-[border-color] duration-200 hover:border-thb04-edge2 focus-visible:outline-2 focus-visible:outline-thb04-acc focus-visible:outline-offset-2">Sign out</button>
      </footer>
    </aside>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-thb04-bg: #fafaf7;
  --color-thb04-bg: oklch(0.98 0.004 80);
  --color-thb04-panel: #ffffff;
  --color-thb04-panel: oklch(1 0 0);
  --color-thb04-hover: #f2f1ec;
  --color-thb04-hover: oklch(0.955 0.007 80);
  --color-thb04-edge: #e5e3dc;
  --color-thb04-edge: oklch(0.9 0.008 80);
  --color-thb04-edge2: #b9b4a8;
  --color-thb04-edge2: oklch(0.75 0.02 60);
  --color-thb04-line: #e8e6df;
  --color-thb04-line: oklch(0.91 0.006 80);
  --color-thb04-ink: #3a3630;
  --color-thb04-ink: oklch(0.25 0.015 60);
  --color-thb04-mut: #7d7669;
  --color-thb04-mut: oklch(0.55 0.015 60);
  --color-thb04-acc: #c2643a;
  --color-thb04-acc: oklch(0.62 0.15 40);
  --font-thb04-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb04-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb04: cubic-bezier(0.32, 0.72, 0.28, 1);
  --ease-thb04in: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
   moment before the browser build has compiled its utilities. */

.thb-04 {
  width: 100%;
  min-height: 100svh;
}
```

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

How this works

Three layers, one attribute. Content, scrim, drawer — and both moving parts read data-open on the stage:

<button class="absolute inset-0 z-2 invisible opacity-0
  bg-[oklch(0.25_0.02_50/0.45)] backdrop-blur-[3px]
  group-data-open:visible group-data-open:opacity-100"></button>

<aside class="absolute inset-y-0 right-0 z-3 w-[min(84%,320px)]
  invisible translate-x-full
  group-data-open:visible group-data-open:translate-x-0">

Why translate-x-full and not -right-80. Offsets are layout properties: animating them reflows every frame and stutters on mid-range phones. A transform moves a finished paint on the compositor. This is the single most common off-canvas bug, and the utility name makes the right choice the shorter one to type.

The scrim is the quiet accessibility win. It is a real <button tabindex="-1" aria-hidden="true">: as a button it takes the click that means “close”, so there are no document-level listeners and no bubbling edge cases. It is hidden from AT because Escape and the drawer's × already serve keyboard and screen-reader users — the scrim is a pointer affordance only.

Visibility, delayed. A closed drawer must leave the tab order, but flipping visibility instantly would cut the slide short. The arbitrary [transition:translate_.4s_var(--ease-thb04),visibility_0s_.4s] gives visibility a delay on close and none on open — per-property delays that Tailwind's delay-* scale cannot express.

Make it yours

  • Drawer side: swap right-0 translate-x-full for left-0 -translate-x-full. That is the entire left-drawer edit.
  • Width: w-[min(84%,320px)] — the 84% guarantees a visible sliver of page (the user's way back out), the 320px caps it on tablets.
  • Scrim recipe: darkness in the oklch alpha, material in backdrop-blur-*. Drop the blur for a flat dim, or go backdrop-blur-md for heavy glass.
  • Stagger tempo: the 45ms multiplier in the links' [transition-delay:calc(120ms+var(--i)*45ms)].
  • Production: change the three absolute utilities to fixed, delete h-135, and lock body scroll while open (one line of JS).

Gotchas — read before shipping

  • Never animate right-*/left-* for a drawer. translate-x-* only — layout-thrashing jank is THE classic off-canvas failure.
  • Without delayed visibility (or inert) the closed drawer's links stay keyboard-reachable off-screen: an instant WCAG failure and a real confusion for screen-reader users.
  • backdrop-blur-* must sit on its own element, not on a pseudo-element of the drawer — the drawer's transform creates a containing block that clips the blur to the panel.
  • iOS momentum-scrolls the page behind the drawer unless you lock body scroll or put overscroll-contain on the drawer's own scroller. Both are in the markup.
  • group-data-open: needs group on the stage, which is also the element the script toggles — keep them the same element or nothing opens.
  • The scrim button needs border-0 p-0: a UA-styled button inside a full-bleed overlay otherwise paints its own border over your dim.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

translate and visibility transitions are ancient, and backdrop-filter has been universal since Firefox 103; the floor quoted here is Tailwind v4's own generated sheet.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…