28 Tailwind Hamburger Menus
20 / 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 Full Screen Blur Overlay Hamburger Menu

The cinematic takeover: the article behind stays visible but slips out of focus while the links arrive through a blur-to-sharp animation with tracking that tightens as they land. Two blurs doing opposite jobs — backdrop-blur-* on the sheet, blur-* on the links — and Tailwind's naming makes the distinction, which trips up half the implementations of this effect, impossible to confuse.

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-20 w-full min-h-svh grid justify-center content-start bg-thb20-bg font-thb20-sans text-thb20-ink px-5 py-9" aria-label="Fullscreen blur overlay menu demo">
  <div class="group relative w-[min(400px,88vw)] h-140 rounded-3xl overflow-hidden border border-thb20-edge bg-thb20-bg bg-[radial-gradient(130%_90%_at_80%_-10%,oklch(0.34_0.08_300/0.8),transparent_55%),radial-gradient(100%_80%_at_0%_110%,oklch(0.3_0.07_220/0.7),transparent_50%)]">
    <article class="thb20-page grid content-start gap-3.5 px-7 pt-24 pb-7 transition-[scale] duration-500 ease-thb20" aria-hidden="true">
      <span class="text-[11px] font-bold tracking-[0.22em] uppercase text-thb20-acc">Field Notes &middot; 07</span>
      <h2 class="text-[30px] font-bold leading-[1.06] tracking-[-0.02em]">The city at f/1.4</h2>
      <p class="text-[13.5px] leading-relaxed text-thb20-mut text-pretty">Night photography is an argument between what the eye saw and what the sensor kept. The lens holds one plane in confidence and lets the rest dissolve into rumor &mdash;</p>
      <p class="text-[13.5px] leading-relaxed text-thb20-mut text-pretty">streetlights become coins of light, faces become suggestions. Depth of field is an editor: it decides, frame by frame, what the story is about.</p>
      <p class="text-[13.5px] leading-relaxed text-thb20-mut text-pretty">The same is true of interfaces. Focus is a resource you spend.</p>
    </article>
    <nav id="thb20-sheet" aria-label="Site" class="thb20-sheet absolute inset-0 z-2 flex flex-col justify-center gap-1 px-8.5 bg-[oklch(0.15_0.02_280/0.55)] backdrop-blur-[18px] backdrop-saturate-[1.2] opacity-0 invisible [transition:opacity_.45s,visibility_0s_.45s] group-data-open:opacity-100 group-data-open:visible group-data-open:[transition:opacity_.45s,visibility_0s_0s]">
      <a href="#stories" style="--i:0" class="py-3.25 rounded-md text-[34px] font-bold tracking-[0.12em] text-thb20-ink no-underline opacity-0 blur-[14px] [transition:opacity_.4s,filter_.5s,letter-spacing_.6s_var(--ease-thb20),color_.2s] hover:text-thb20-acc focus-visible:text-thb20-acc focus-visible:outline-2 focus-visible:outline-thb20-acc focus-visible:outline-offset-5 group-data-open:opacity-100 group-data-open:blur-none group-data-open:tracking-normal group-data-open:[transition-delay:calc(150ms+var(--i)*70ms)] motion-reduce:blur-none motion-reduce:tracking-normal motion-reduce:[transition:opacity_.15s]">Stories</a>
      <a href="#prints" style="--i:1" class="py-3.25 rounded-md text-[34px] font-bold tracking-[0.12em] text-thb20-ink no-underline opacity-0 blur-[14px] [transition:opacity_.4s,filter_.5s,letter-spacing_.6s_var(--ease-thb20),color_.2s] hover:text-thb20-acc focus-visible:text-thb20-acc focus-visible:outline-2 focus-visible:outline-thb20-acc focus-visible:outline-offset-5 group-data-open:opacity-100 group-data-open:blur-none group-data-open:tracking-normal group-data-open:[transition-delay:calc(150ms+var(--i)*70ms)] motion-reduce:blur-none motion-reduce:tracking-normal motion-reduce:[transition:opacity_.15s]">Prints</a>
      <a href="#workshops" style="--i:2" class="py-3.25 rounded-md text-[34px] font-bold tracking-[0.12em] text-thb20-ink no-underline opacity-0 blur-[14px] [transition:opacity_.4s,filter_.5s,letter-spacing_.6s_var(--ease-thb20),color_.2s] hover:text-thb20-acc focus-visible:text-thb20-acc focus-visible:outline-2 focus-visible:outline-thb20-acc focus-visible:outline-offset-5 group-data-open:opacity-100 group-data-open:blur-none group-data-open:tracking-normal group-data-open:[transition-delay:calc(150ms+var(--i)*70ms)] motion-reduce:blur-none motion-reduce:tracking-normal motion-reduce:[transition:opacity_.15s]">Workshops</a>
      <a href="#about" style="--i:3" class="py-3.25 rounded-md text-[34px] font-bold tracking-[0.12em] text-thb20-ink no-underline opacity-0 blur-[14px] [transition:opacity_.4s,filter_.5s,letter-spacing_.6s_var(--ease-thb20),color_.2s] hover:text-thb20-acc focus-visible:text-thb20-acc focus-visible:outline-2 focus-visible:outline-thb20-acc focus-visible:outline-offset-5 group-data-open:opacity-100 group-data-open:blur-none group-data-open:tracking-normal group-data-open:[transition-delay:calc(150ms+var(--i)*70ms)] motion-reduce:blur-none motion-reduce:tracking-normal motion-reduce:[transition:opacity_.15s]">About</a>
      <span style="--i:4" class="mt-6.5 font-thb20-mono text-xs tracking-[0.06em] text-thb20-mut opacity-0 blur-[10px] [transition:opacity_.4s,filter_.5s] group-data-open:opacity-100 group-data-open:blur-none group-data-open:[transition-delay:calc(150ms+var(--i)*70ms)] motion-reduce:blur-none" aria-hidden="true">&fnof;/1.4 &middot; everything else can wait</span>
    </nav>
    <header class="absolute inset-x-0 top-0 z-3 flex items-center justify-between h-19 pl-6.5 pr-5">
      <span class="text-[13px] font-extrabold tracking-[0.3em]">APERTURE&amp;CO</span>
      <button type="button" aria-expanded="false" aria-controls="thb20-sheet" aria-label="Open menu" class="thb20-btn grid place-items-center w-11.5 h-11.5 rounded-full border border-[oklch(1_0_0/0.22)] bg-[oklch(1_0_0/0.06)] backdrop-blur-[10px] text-thb20-ink cursor-pointer transition-[background,scale] duration-250 hover:bg-[oklch(1_0_0/0.14)] hover:scale-105 active:scale-94 focus-visible:outline-2 focus-visible:outline-thb20-acc focus-visible:outline-offset-3">
        <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>
    </header>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-thb20-bg: #2b2740;
  --color-thb20-bg: oklch(0.2 0.025 280);
  --color-thb20-edge: #4c4666;
  --color-thb20-edge: oklch(0.32 0.03 280);
  --color-thb20-ink: #eae7f2;
  --color-thb20-ink: oklch(0.93 0.01 280);
  --color-thb20-mut: #a9a3ba;
  --color-thb20-mut: oklch(0.68 0.02 280);
  --color-thb20-acc: #e0a94e;
  --color-thb20-acc: oklch(0.8 0.13 60);
  --font-thb20-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb20-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb20: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Same rule as demo 07: without a real fallback, an engine with no backdrop-filter
   paints a 55%-alpha sheet and the article reads straight through the links. */

@supports not (backdrop-filter: blur(1px)) {
  .thb-20 .thb20-sheet {
    background: oklch(0.17 0.025 280 / 0.93);
  }

  .thb-20 .thb20-btn {
    background: oklch(0.3 0.05 280 / 0.85);
  }

  .thb-20 .group[data-open] .thb20-page {
    scale: 0.96;
  }
}
/* 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-20 {
  width: 100%;
  min-height: 100svh;
}
(function () {
  document.querySelectorAll('.thb-20 > .group').forEach(function (stage) {
    if (stage.dataset.bound) return; stage.dataset.bound = '1';
    var btn = stage.querySelector('button[aria-controls="thb20-sheet"]');
    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-thb20-bg: #2b2740;
  --color-thb20-bg: oklch(0.2 0.025 280);
  --color-thb20-edge: #4c4666;
  --color-thb20-edge: oklch(0.32 0.03 280);
  --color-thb20-ink: #eae7f2;
  --color-thb20-ink: oklch(0.93 0.01 280);
  --color-thb20-mut: #a9a3ba;
  --color-thb20-mut: oklch(0.68 0.02 280);
  --color-thb20-acc: #e0a94e;
  --color-thb20-acc: oklch(0.8 0.13 60);
  --font-thb20-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb20-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb20: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Same rule as demo 07: without a real fallback, an engine with no backdrop-filter
   paints a 55%-alpha sheet and the article reads straight through the links. */

@supports not (backdrop-filter: blur(1px)) {
  .thb-20 .thb20-sheet {
    background: oklch(0.17 0.025 280 / 0.93);
  }

  .thb-20 .thb20-btn {
    background: oklch(0.3 0.05 280 / 0.85);
  }

  .thb-20 .group[data-open] .thb20-page {
    scale: 0.96;
  }
}
/* 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-20 {
  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;800&family=JetBrains+Mono:wght@400;500;600&display=swap">

What changed in the port

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

  • The port's clearest pedagogical win: backdrop-blur-* versus blur-* are different words for the two blurs this effect needs, so the mistake that ruins most implementations of it becomes hard to make.
  • The prefixed -webkit-backdrop-filter duplicates disappear on both the sheet and the button — Tailwind emits them.
  • The links' three-property arrival (opacity, filter, letter-spacing) keeps an arbitrary [transition:…] shorthand, because the three need different durations and only two of them take the delay.
  • tracking-[0.12em] → tracking-normal replaces two letter-spacing declarations, and pairs visibly with the blur that sells the effect.
  • The @supports not fallback stays CSS and now also has to reach the page element through the state attribute, which is the one selector in this demo a variant cannot express.
  • motion-reduce:blur-none motion-reduce:tracking-normal removes the rasterised text entirely for reduced-motion readers rather than merely speeding it up.
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 Full Screen Blur Overlay Hamburger Menu
Source: https://codefronts.com/navigation/tailwind-hamburger-menus/tailwind-full-screen-blur-overlay-hamburger-menu/

The cinematic takeover: the article behind stays visible but slips out of focus while the links arrive through a blur-to-sharp animation with tracking that tightens as they land. Two blurs doing opposite jobs &mdash; backdrop-blur-* on the sheet, blur-* on the links &mdash; and Tailwind's naming makes the distinction, which trips up half the implementations of this effect, impossible to confuse.
## HTML
```html
<section class="thb-20 w-full min-h-svh grid justify-center content-start bg-thb20-bg font-thb20-sans text-thb20-ink px-5 py-9" aria-label="Fullscreen blur overlay menu demo">
  <div class="group relative w-[min(400px,88vw)] h-140 rounded-3xl overflow-hidden border border-thb20-edge bg-thb20-bg bg-[radial-gradient(130%_90%_at_80%_-10%,oklch(0.34_0.08_300/0.8),transparent_55%),radial-gradient(100%_80%_at_0%_110%,oklch(0.3_0.07_220/0.7),transparent_50%)]">
    <article class="thb20-page grid content-start gap-3.5 px-7 pt-24 pb-7 transition-[scale] duration-500 ease-thb20" aria-hidden="true">
      <span class="text-[11px] font-bold tracking-[0.22em] uppercase text-thb20-acc">Field Notes &middot; 07</span>
      <h2 class="text-[30px] font-bold leading-[1.06] tracking-[-0.02em]">The city at f/1.4</h2>
      <p class="text-[13.5px] leading-relaxed text-thb20-mut text-pretty">Night photography is an argument between what the eye saw and what the sensor kept. The lens holds one plane in confidence and lets the rest dissolve into rumor &mdash;</p>
      <p class="text-[13.5px] leading-relaxed text-thb20-mut text-pretty">streetlights become coins of light, faces become suggestions. Depth of field is an editor: it decides, frame by frame, what the story is about.</p>
      <p class="text-[13.5px] leading-relaxed text-thb20-mut text-pretty">The same is true of interfaces. Focus is a resource you spend.</p>
    </article>
    <nav id="thb20-sheet" aria-label="Site" class="thb20-sheet absolute inset-0 z-2 flex flex-col justify-center gap-1 px-8.5 bg-[oklch(0.15_0.02_280/0.55)] backdrop-blur-[18px] backdrop-saturate-[1.2] opacity-0 invisible [transition:opacity_.45s,visibility_0s_.45s] group-data-open:opacity-100 group-data-open:visible group-data-open:[transition:opacity_.45s,visibility_0s_0s]">
      <a href="#stories" style="--i:0" class="py-3.25 rounded-md text-[34px] font-bold tracking-[0.12em] text-thb20-ink no-underline opacity-0 blur-[14px] [transition:opacity_.4s,filter_.5s,letter-spacing_.6s_var(--ease-thb20),color_.2s] hover:text-thb20-acc focus-visible:text-thb20-acc focus-visible:outline-2 focus-visible:outline-thb20-acc focus-visible:outline-offset-5 group-data-open:opacity-100 group-data-open:blur-none group-data-open:tracking-normal group-data-open:[transition-delay:calc(150ms+var(--i)*70ms)] motion-reduce:blur-none motion-reduce:tracking-normal motion-reduce:[transition:opacity_.15s]">Stories</a>
      <a href="#prints" style="--i:1" class="py-3.25 rounded-md text-[34px] font-bold tracking-[0.12em] text-thb20-ink no-underline opacity-0 blur-[14px] [transition:opacity_.4s,filter_.5s,letter-spacing_.6s_var(--ease-thb20),color_.2s] hover:text-thb20-acc focus-visible:text-thb20-acc focus-visible:outline-2 focus-visible:outline-thb20-acc focus-visible:outline-offset-5 group-data-open:opacity-100 group-data-open:blur-none group-data-open:tracking-normal group-data-open:[transition-delay:calc(150ms+var(--i)*70ms)] motion-reduce:blur-none motion-reduce:tracking-normal motion-reduce:[transition:opacity_.15s]">Prints</a>
      <a href="#workshops" style="--i:2" class="py-3.25 rounded-md text-[34px] font-bold tracking-[0.12em] text-thb20-ink no-underline opacity-0 blur-[14px] [transition:opacity_.4s,filter_.5s,letter-spacing_.6s_var(--ease-thb20),color_.2s] hover:text-thb20-acc focus-visible:text-thb20-acc focus-visible:outline-2 focus-visible:outline-thb20-acc focus-visible:outline-offset-5 group-data-open:opacity-100 group-data-open:blur-none group-data-open:tracking-normal group-data-open:[transition-delay:calc(150ms+var(--i)*70ms)] motion-reduce:blur-none motion-reduce:tracking-normal motion-reduce:[transition:opacity_.15s]">Workshops</a>
      <a href="#about" style="--i:3" class="py-3.25 rounded-md text-[34px] font-bold tracking-[0.12em] text-thb20-ink no-underline opacity-0 blur-[14px] [transition:opacity_.4s,filter_.5s,letter-spacing_.6s_var(--ease-thb20),color_.2s] hover:text-thb20-acc focus-visible:text-thb20-acc focus-visible:outline-2 focus-visible:outline-thb20-acc focus-visible:outline-offset-5 group-data-open:opacity-100 group-data-open:blur-none group-data-open:tracking-normal group-data-open:[transition-delay:calc(150ms+var(--i)*70ms)] motion-reduce:blur-none motion-reduce:tracking-normal motion-reduce:[transition:opacity_.15s]">About</a>
      <span style="--i:4" class="mt-6.5 font-thb20-mono text-xs tracking-[0.06em] text-thb20-mut opacity-0 blur-[10px] [transition:opacity_.4s,filter_.5s] group-data-open:opacity-100 group-data-open:blur-none group-data-open:[transition-delay:calc(150ms+var(--i)*70ms)] motion-reduce:blur-none" aria-hidden="true">&fnof;/1.4 &middot; everything else can wait</span>
    </nav>
    <header class="absolute inset-x-0 top-0 z-3 flex items-center justify-between h-19 pl-6.5 pr-5">
      <span class="text-[13px] font-extrabold tracking-[0.3em]">APERTURE&amp;CO</span>
      <button type="button" aria-expanded="false" aria-controls="thb20-sheet" aria-label="Open menu" class="thb20-btn grid place-items-center w-11.5 h-11.5 rounded-full border border-[oklch(1_0_0/0.22)] bg-[oklch(1_0_0/0.06)] backdrop-blur-[10px] text-thb20-ink cursor-pointer transition-[background,scale] duration-250 hover:bg-[oklch(1_0_0/0.14)] hover:scale-105 active:scale-94 focus-visible:outline-2 focus-visible:outline-thb20-acc focus-visible:outline-offset-3">
        <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>
    </header>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-thb20-bg: #2b2740;
  --color-thb20-bg: oklch(0.2 0.025 280);
  --color-thb20-edge: #4c4666;
  --color-thb20-edge: oklch(0.32 0.03 280);
  --color-thb20-ink: #eae7f2;
  --color-thb20-ink: oklch(0.93 0.01 280);
  --color-thb20-mut: #a9a3ba;
  --color-thb20-mut: oklch(0.68 0.02 280);
  --color-thb20-acc: #e0a94e;
  --color-thb20-acc: oklch(0.8 0.13 60);
  --font-thb20-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-thb20-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-thb20: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Same rule as demo 07: without a real fallback, an engine with no backdrop-filter
   paints a 55%-alpha sheet and the article reads straight through the links. */

@supports not (backdrop-filter: blur(1px)) {
  .thb-20 .thb20-sheet {
    background: oklch(0.17 0.025 280 / 0.93);
  }

  .thb-20 .thb20-btn {
    background: oklch(0.3 0.05 280 / 0.85);
  }

  .thb-20 .group[data-open] .thb20-page {
    scale: 0.96;
  }
}
/* 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-20 {
  width: 100%;
  min-height: 100svh;
}
```

## JavaScript
```js
(function () {
  document.querySelectorAll('.thb-20 > .group').forEach(function (stage) {
    if (stage.dataset.bound) return; stage.dataset.bound = '1';
    var btn = stage.querySelector('button[aria-controls="thb20-sheet"]');
    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

Two blurs, opposite jobs. The sheet blurs the page behind it; the links blur themselves. In hand-written CSS both are spelled filter-ish and get mixed up constantly. Here they are different words:

<!-- the lens -->
<nav class="absolute inset-0 bg-[oklch(0.15_0.02_280/0.55)]
  backdrop-blur-[18px] backdrop-saturate-[1.2]
  opacity-0 invisible group-data-open:opacity-100 group-data-open:visible">

<!-- the type -->
<a class="opacity-0 blur-[14px] tracking-[0.12em]
  group-data-open:opacity-100 group-data-open:blur-none
  group-data-open:tracking-normal">

The performance trick of the whole demo. Animating a backdrop-filter radius forces the engine to recompute the backdrop every frame at every radius. Fading the sheet's opacity instead looks identical and costs a fraction — the blur is computed once and blended in.

Keep element blur small and brief. blur-* on text rasterises the layer during the animation; 14px for half a second is fine, permanent blur on large type is not.

The fallback. No backdrop-filter → the sheet's alpha rises to 93% and the page behind scales down instead, so the mode change still reads without the lens. It stays a feature query in CSS, for the same reason as demo 07.

Make it yours

  • Defocus strength: backdrop-blur-[18px] is menu mode, [8px] keeps the article skimmable (good for docs), [28px] is privacy glass.
  • backdrop-saturate-[1.2] keeps colour alive through the fog. Drop to 1 for a grey editorial mood, or add backdrop-brightness-[0.8] for night mode.
  • Link arrival: the 70ms stagger and the blur-[14px] starting value are the two feel dials. Halve both for a snappier, less cinematic build.
  • The page content is a real article. Keep yours real too — blur over skeleton bars looks like a broken screenshot.
  • Retheme through --color-thb20-acc, which drives the kicker, the hover colour and every focus ring.

Gotchas — read before shipping

  • Never animate the backdrop-blur radius. Fade the sheet's opacity — identical result, a fraction of the cost.
  • blur-* and backdrop-blur-* are different properties. Reaching for the wrong one is the single most common failure in this effect; the class names are your guardrail.
  • The closed sheet needs invisible, not only opacity-0: an invisible backdrop layer still holds compositor memory on some mobile GPUs.
  • Long menus need the SHEET to scroll (overflow-auto on the sheet), not the page. Blurred fixed backdrops plus page scroll double-composite and judder on Android.
  • A stray will-change on blurred text keeps it rasterised after landing. If edges look soft at rest, look for that before suspecting the blur.
  • Under motion-reduce: drop the blur entirely rather than shortening it — a brief blur is still a rasterised flash for a motion-sensitive reader.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

backdrop-filter has been available everywhere since Firefox 103 (prefixed in Safari, and Tailwind emits the prefix); element filter is ancient. The @supports not fallback covers the rest.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…