Tailwind + JSMIT licensed

Tailwind Inert Background Content Behind a Drawer

The half of modal behaviour a focus trap cannot give you: everything behind the drawer is switched off with one attribute. inert takes the background out of the accessibility tree, out of the tab order and out of pointer events at once — and because Tailwind v4 ships an inert: variant, the visual consequence (a dimmed table) is declared on the element that goes inert, next to its normal state.

Live Demo
Plain CSSTailwind v4
Try it

Mosaic by Cruip: a complete Tailwind admin dashboard template in HTML, React, Next.js and Vue · $69

Cruip · Mosaic by Cruip · Affiliate link

View Mosaic dashboard

The code

<section class="tsb-03 block relative w-full min-h-svh bg-tsb03-bg text-tsb03-ink font-tsb03-sans leading-[1.55]" aria-label="Sable order desk">
  <div class="absolute top-3.5 right-3.5 z-60">
    <input class="tsb-03-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-03-theme">
    <label class="grid place-items-center w-11 h-11 rounded-full border border-tsb03-line bg-tsb03-surf text-tsb03-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-tsb03-acc peer-focus-visible:outline-offset-2" for="tsb-03-theme">
      <svg class="w-4.5 h-4.5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
      <span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Toggle dark theme</span>
    </label>
  </div>

  <div class="group relative overflow-hidden min-h-svh bg-tsb03-surf">
    <div class="min-w-0 transition-opacity duration-200 inert:opacity-45 motion-reduce:transition-none" id="tsb-03-content">
      <div class="flex flex-wrap items-center gap-1.5 py-2.5 pl-4 pr-[4.2rem] bg-tsb03-surf2 border-b border-tsb03-line">
        <p class="mr-auto m-0 font-tsb03-display text-[1.05rem]">Sable &mdash; order desk</p>
        <a class="inline-flex items-center min-h-11 px-2.5 text-[0.88rem] font-semibold text-tsb03-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:outline-offset-2" href="#tsb-03-content">Fulfilment</a>
        <a class="inline-flex items-center min-h-11 px-2.5 text-[0.88rem] font-semibold text-tsb03-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:outline-offset-2" href="#tsb-03-content">Returns</a>
        <button class="min-h-11 px-4 border border-tsb03-acc bg-tsb03-acc text-tsb03-surf font-tsb03-sans text-[0.9rem] font-semibold cursor-pointer focus-visible:outline-2 focus-visible:outline-tsb03-ink focus-visible:outline-offset-2" type="button" id="tsb-03-open" aria-expanded="false" aria-controls="tsb-03-drawer">Filters</button>
      </div>
      <table class="w-full border-collapse text-[0.9rem]">
        <thead><tr class="[&>th]:px-4 [&>th]:py-3 [&>th]:text-start [&>th]:border-b [&>th]:border-tsb03-line [&>th]:text-[0.74rem] [&>th]:tracking-[0.06em] [&>th]:uppercase [&>th]:font-semibold [&>th]:text-tsb03-mut"><th scope="col">Order</th><th scope="col">Customer</th><th scope="col">State</th><th scope="col"></th></tr></thead>
        <tbody class="[&>tr>td]:px-4 [&>tr>td]:py-3 [&>tr>td]:border-b [&>tr>td]:border-tsb03-line [&>tr>td]:min-w-0">
          <tr><td>SB-4417</td><td>Priya Sharma</td><td>Packing</td><td><a class="inline-flex items-center min-h-11 px-2.5 text-[0.88rem] font-semibold text-tsb03-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:outline-offset-2" href="#tsb-03-content">Open</a></td></tr>
          <tr><td>SB-4418</td><td>Jordan Lee</td><td>Awaiting stock</td><td><a class="inline-flex items-center min-h-11 px-2.5 text-[0.88rem] font-semibold text-tsb03-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:outline-offset-2" href="#tsb-03-content">Open</a></td></tr>
          <tr><td>SB-4421</td><td>Sam Rivera</td><td>Ready to ship</td><td><a class="inline-flex items-center min-h-11 px-2.5 text-[0.88rem] font-semibold text-tsb03-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:outline-offset-2" href="#tsb-03-content">Open</a></td></tr>
          <tr><td>SB-4423</td><td>Ada Okafor</td><td>Packing</td><td><a class="inline-flex items-center min-h-11 px-2.5 text-[0.88rem] font-semibold text-tsb03-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:outline-offset-2" href="#tsb-03-content">Open</a></td></tr>
          <tr><td>SB-4425</td><td>Tomas Vidal</td><td>Awaiting stock</td><td><a class="inline-flex items-center min-h-11 px-2.5 text-[0.88rem] font-semibold text-tsb03-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:outline-offset-2" href="#tsb-03-content">Open</a></td></tr>
        </tbody>
      </table>
    </div>

    <div class="absolute inset-0 z-20 bg-[oklch(25%_0.02_60/0.42)] [&[hidden]]:hidden" data-tsb-03-scrim hidden></div>

    <aside class="absolute inset-y-0 right-0 z-30 flex flex-col w-[min(18rem,84%)] bg-tsb03-surf border-l border-tsb03-line translate-x-full transition-transform duration-[220ms] ease-out group-data-open:translate-x-0 motion-reduce:duration-[1ms] [&[hidden]]:hidden" id="tsb-03-drawer" role="dialog" aria-modal="true" aria-labelledby="tsb-03-dh" hidden>
      <header class="flex items-center gap-2 px-3 py-2.5 border-b border-tsb03-line">
        <h3 class="mr-auto m-0 font-tsb03-display text-[1.1rem]" id="tsb-03-dh">Filters</h3>
        <button class="grid place-items-center w-11 h-11 border border-tsb03-line bg-transparent text-tsb03-ink cursor-pointer [&>svg]:w-4 [&>svg]:h-4 focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:-outline-offset-2" type="button" data-tsb-03-close>
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6L6 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
          <span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Close filters</span>
        </button>
      </header>
      <nav class="flex-1 min-h-0 overflow-y-auto [overscroll-behavior:contain]" aria-label="Order views">
        <ul class="list-none m-0 p-0">
          <li><a class="flex items-center min-w-0 min-h-11 px-3 py-2 text-[0.9rem] text-tsb03-ink no-underline border-b border-tsb03-line hover:bg-tsb03-surf2 focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb03-accsoft aria-[current=page]:text-tsb03-acc aria-[current=page]:font-semibold" href="#tsb-03-content" aria-current="page">All open orders</a></li>
          <li><a class="flex items-center min-w-0 min-h-11 px-3 py-2 text-[0.9rem] text-tsb03-ink no-underline border-b border-tsb03-line hover:bg-tsb03-surf2 focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:-outline-offset-2" href="#tsb-03-content">Awaiting stock</a></li>
          <li><a class="flex items-center min-w-0 min-h-11 px-3 py-2 text-[0.9rem] text-tsb03-ink no-underline border-b border-tsb03-line hover:bg-tsb03-surf2 focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:-outline-offset-2" href="#tsb-03-content">Ready to ship</a></li>
          <li><a class="flex items-center min-w-0 min-h-11 px-3 py-2 text-[0.9rem] text-tsb03-ink no-underline border-b border-tsb03-line hover:bg-tsb03-surf2 focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:-outline-offset-2" href="#tsb-03-content">Returns queue</a></li>
          <li><a class="flex items-center min-w-0 min-h-11 px-3 py-2 text-[0.9rem] text-tsb03-ink no-underline border-b border-tsb03-line hover:bg-tsb03-surf2 focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:-outline-offset-2" href="#tsb-03-content">Archived</a></li>
        </ul>
      </nav>
    </aside>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tsb03-bg: #f6f1e8;
  --color-tsb03-bg: oklch(95% 0.018 85);
  --color-tsb03-surf: #fffdf9;
  --color-tsb03-surf: oklch(99% 0.008 85);
  --color-tsb03-surf2: #efe7da;
  --color-tsb03-surf2: oklch(92% 0.022 80);
  --color-tsb03-line: #ddd1bd;
  --color-tsb03-line: oklch(85% 0.032 80);
  --color-tsb03-ink: #23201a;
  --color-tsb03-ink: oklch(24% 0.012 70);
  --color-tsb03-mut: #6d6455;
  --color-tsb03-mut: oklch(48% 0.02 75);
  --color-tsb03-acc: #8a4b1f;
  --color-tsb03-acc: oklch(48% 0.12 52);
  --color-tsb03-accsoft: #f2e4d5;
  --color-tsb03-accsoft: oklch(92% 0.03 60);
  --font-tsb03-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-tsb03-display: 'Source Serif 4', 'Iowan Old Style', Palatino, Georgia, serif;
}
/* 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. */

.tsb-03 {
  width: 100%;
  min-height: 100svh;
}

.tsb-03 *,
.tsb-03 *::before,
.tsb-03 *::after {
  box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */

.tsb-03:has(.tsb-03-theme-input:checked) {
  --color-tsb03-bg: #17140f;
  --color-tsb03-surf: #1f1b15;
  --color-tsb03-surf2: #292319;
  --color-tsb03-line: #3a3226;
  --color-tsb03-ink: #f2ece1;
  --color-tsb03-mut: #a89b87;
  --color-tsb03-acc: #e6a06a;
  --color-tsb03-acc: oklch(78% 0.11 55);
  --color-tsb03-accsoft: #35291d;
}

@media (prefers-color-scheme: dark) {
  .tsb-03 {
    --color-tsb03-bg: #17140f;
    --color-tsb03-surf: #1f1b15;
    --color-tsb03-surf2: #292319;
    --color-tsb03-line: #3a3226;
    --color-tsb03-ink: #f2ece1;
    --color-tsb03-mut: #a89b87;
    --color-tsb03-acc: #e6a06a;
    --color-tsb03-acc: oklch(78% 0.11 55);
    --color-tsb03-accsoft: #35291d;
  }

  .tsb-03:has(.tsb-03-theme-input:checked) {
    --color-tsb03-bg: #f6f1e8;
    --color-tsb03-surf: #fffdf9;
    --color-tsb03-surf2: #efe7da;
    --color-tsb03-line: #ddd1bd;
    --color-tsb03-ink: #23201a;
    --color-tsb03-mut: #6d6455;
    --color-tsb03-acc: #8a4b1f;
    --color-tsb03-acc: oklch(48% 0.12 52);
    --color-tsb03-accsoft: #f2e4d5;
  }
}

@media (prefers-color-scheme: light) {
  .tsb-03:has(.tsb-03-theme-input:checked) {
    --color-tsb03-bg: #17140f;
    --color-tsb03-surf: #1f1b15;
    --color-tsb03-surf2: #292319;
    --color-tsb03-line: #3a3226;
    --color-tsb03-ink: #f2ece1;
    --color-tsb03-mut: #a89b87;
    --color-tsb03-acc: #e6a06a;
    --color-tsb03-acc: oklch(78% 0.11 55);
    --color-tsb03-accsoft: #35291d;
  }
}
(function () {
  document.querySelectorAll('.tsb-03').forEach(function (root) {
    if (root.dataset.bound) return; root.dataset.bound = '1';
    var stage = root.querySelector('.group'),
        drawer = root.querySelector('#tsb-03-drawer'),
        scrim = root.querySelector('[data-tsb-03-scrim]'),
        content = root.querySelector('#tsb-03-content'),
        openBtn = root.querySelector('#tsb-03-open'),
        supported = 'inert' in HTMLElement.prototype,
        returnTo = null;
    function behind() { return Array.prototype.slice.call(content.querySelectorAll('a[href], button:not([disabled])')); }
    function block(on) {
      if (supported) { content.inert = on; return; }
      content.setAttribute('aria-hidden', String(on));
      behind().forEach(function (el) { on ? el.setAttribute('tabindex', '-1') : el.removeAttribute('tabindex'); });
    }
    function onKey(e) { if (e.key === 'Escape') setOpen(false); }
    function setOpen(next) {
      if (next) returnTo = document.activeElement;
      drawer.hidden = scrim.hidden = !next;
      stage.toggleAttribute('data-open', next);
      openBtn.setAttribute('aria-expanded', String(next));
      block(next);
      if (next) {
        document.addEventListener('keydown', onKey);
        requestAnimationFrame(function () { drawer.querySelector('[data-tsb-03-close]').focus(); });
      } else {
        document.removeEventListener('keydown', onKey);
        (returnTo || openBtn).focus({ preventScroll: true });
      }
    }
    openBtn.addEventListener('click', function () { setOpen(true); });
    scrim.addEventListener('click', function () { setOpen(false); });
    root.querySelectorAll('[data-tsb-03-close]').forEach(function (b) { b.addEventListener('click', function () { setOpen(false); }); });
    root.__tsbCleanup = function () { document.removeEventListener('keydown', onKey); };
  });
})();

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-tsb03-bg: #f6f1e8;
  --color-tsb03-bg: oklch(95% 0.018 85);
  --color-tsb03-surf: #fffdf9;
  --color-tsb03-surf: oklch(99% 0.008 85);
  --color-tsb03-surf2: #efe7da;
  --color-tsb03-surf2: oklch(92% 0.022 80);
  --color-tsb03-line: #ddd1bd;
  --color-tsb03-line: oklch(85% 0.032 80);
  --color-tsb03-ink: #23201a;
  --color-tsb03-ink: oklch(24% 0.012 70);
  --color-tsb03-mut: #6d6455;
  --color-tsb03-mut: oklch(48% 0.02 75);
  --color-tsb03-acc: #8a4b1f;
  --color-tsb03-acc: oklch(48% 0.12 52);
  --color-tsb03-accsoft: #f2e4d5;
  --color-tsb03-accsoft: oklch(92% 0.03 60);
  --font-tsb03-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-tsb03-display: 'Source Serif 4', 'Iowan Old Style', Palatino, Georgia, serif;
}
/* 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. */

.tsb-03 {
  width: 100%;
  min-height: 100svh;
}

.tsb-03 *,
.tsb-03 *::before,
.tsb-03 *::after {
  box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */

.tsb-03:has(.tsb-03-theme-input:checked) {
  --color-tsb03-bg: #17140f;
  --color-tsb03-surf: #1f1b15;
  --color-tsb03-surf2: #292319;
  --color-tsb03-line: #3a3226;
  --color-tsb03-ink: #f2ece1;
  --color-tsb03-mut: #a89b87;
  --color-tsb03-acc: #e6a06a;
  --color-tsb03-acc: oklch(78% 0.11 55);
  --color-tsb03-accsoft: #35291d;
}

@media (prefers-color-scheme: dark) {
  .tsb-03 {
    --color-tsb03-bg: #17140f;
    --color-tsb03-surf: #1f1b15;
    --color-tsb03-surf2: #292319;
    --color-tsb03-line: #3a3226;
    --color-tsb03-ink: #f2ece1;
    --color-tsb03-mut: #a89b87;
    --color-tsb03-acc: #e6a06a;
    --color-tsb03-acc: oklch(78% 0.11 55);
    --color-tsb03-accsoft: #35291d;
  }

  .tsb-03:has(.tsb-03-theme-input:checked) {
    --color-tsb03-bg: #f6f1e8;
    --color-tsb03-surf: #fffdf9;
    --color-tsb03-surf2: #efe7da;
    --color-tsb03-line: #ddd1bd;
    --color-tsb03-ink: #23201a;
    --color-tsb03-mut: #6d6455;
    --color-tsb03-acc: #8a4b1f;
    --color-tsb03-acc: oklch(48% 0.12 52);
    --color-tsb03-accsoft: #f2e4d5;
  }
}

@media (prefers-color-scheme: light) {
  .tsb-03:has(.tsb-03-theme-input:checked) {
    --color-tsb03-bg: #17140f;
    --color-tsb03-surf: #1f1b15;
    --color-tsb03-surf2: #292319;
    --color-tsb03-line: #3a3226;
    --color-tsb03-ink: #f2ece1;
    --color-tsb03-mut: #a89b87;
    --color-tsb03-acc: #e6a06a;
    --color-tsb03-acc: oklch(78% 0.11 55);
    --color-tsb03-accsoft: #35291d;
  }
}
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=Source+Serif+4:opsz,wght@8..60,400;8..60,600;8..60,700&display=swap">

What changed in the port

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

  • .content[inert] { opacity: .45 } becomes inert:opacity-45 — v4 ships the variant, so the off state is declared on the element that goes off.
  • The right-edge drawer states both positions on itself: translate-x-full resting, group-data-open:translate-x-0 open. Mirroring the demo to the left edge is two character-level edits.
  • The 46rem media query is not forced onto Tailwind's md: --breakpoint-r46 in @theme mints an r46: variant, so the port keeps the original number.
  • overscroll-behavior and clip-path: inset(50%) stay arbitrary properties — neither is a design-scale value, and Tailwind is right not to ship them.
  • The inert fallback stays in script. Where the attribute is unsupported the inert: variant simply never matches, so the background is switched off without being dimmed — the safe direction to fail.
  • The scrim and drawer are siblings of the inert subtree, not children. That is a markup rule the class list cannot enforce, and the most common way this pattern is broken.
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 Inert Background Content Behind a Drawer
Source: https://codefronts.com/navigation/tailwind-css-sidebars/tailwind-inert-background-content-behind-a-drawer/

The half of modal behaviour a focus trap cannot give you: everything behind the drawer is switched off with one attribute. inert takes the background out of the accessibility tree, out of the tab order and out of pointer events at once &mdash; and because Tailwind v4 ships an inert: variant, the visual consequence (a dimmed table) is declared on the element that goes inert, next to its normal state.
## HTML
```html
<section class="tsb-03 block relative w-full min-h-svh bg-tsb03-bg text-tsb03-ink font-tsb03-sans leading-[1.55]" aria-label="Sable order desk">
  <div class="absolute top-3.5 right-3.5 z-60">
    <input class="tsb-03-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-03-theme">
    <label class="grid place-items-center w-11 h-11 rounded-full border border-tsb03-line bg-tsb03-surf text-tsb03-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-tsb03-acc peer-focus-visible:outline-offset-2" for="tsb-03-theme">
      <svg class="w-4.5 h-4.5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
      <span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Toggle dark theme</span>
    </label>
  </div>

  <div class="group relative overflow-hidden min-h-svh bg-tsb03-surf">
    <div class="min-w-0 transition-opacity duration-200 inert:opacity-45 motion-reduce:transition-none" id="tsb-03-content">
      <div class="flex flex-wrap items-center gap-1.5 py-2.5 pl-4 pr-[4.2rem] bg-tsb03-surf2 border-b border-tsb03-line">
        <p class="mr-auto m-0 font-tsb03-display text-[1.05rem]">Sable &mdash; order desk</p>
        <a class="inline-flex items-center min-h-11 px-2.5 text-[0.88rem] font-semibold text-tsb03-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:outline-offset-2" href="#tsb-03-content">Fulfilment</a>
        <a class="inline-flex items-center min-h-11 px-2.5 text-[0.88rem] font-semibold text-tsb03-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:outline-offset-2" href="#tsb-03-content">Returns</a>
        <button class="min-h-11 px-4 border border-tsb03-acc bg-tsb03-acc text-tsb03-surf font-tsb03-sans text-[0.9rem] font-semibold cursor-pointer focus-visible:outline-2 focus-visible:outline-tsb03-ink focus-visible:outline-offset-2" type="button" id="tsb-03-open" aria-expanded="false" aria-controls="tsb-03-drawer">Filters</button>
      </div>
      <table class="w-full border-collapse text-[0.9rem]">
        <thead><tr class="[&>th]:px-4 [&>th]:py-3 [&>th]:text-start [&>th]:border-b [&>th]:border-tsb03-line [&>th]:text-[0.74rem] [&>th]:tracking-[0.06em] [&>th]:uppercase [&>th]:font-semibold [&>th]:text-tsb03-mut"><th scope="col">Order</th><th scope="col">Customer</th><th scope="col">State</th><th scope="col"></th></tr></thead>
        <tbody class="[&>tr>td]:px-4 [&>tr>td]:py-3 [&>tr>td]:border-b [&>tr>td]:border-tsb03-line [&>tr>td]:min-w-0">
          <tr><td>SB-4417</td><td>Priya Sharma</td><td>Packing</td><td><a class="inline-flex items-center min-h-11 px-2.5 text-[0.88rem] font-semibold text-tsb03-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:outline-offset-2" href="#tsb-03-content">Open</a></td></tr>
          <tr><td>SB-4418</td><td>Jordan Lee</td><td>Awaiting stock</td><td><a class="inline-flex items-center min-h-11 px-2.5 text-[0.88rem] font-semibold text-tsb03-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:outline-offset-2" href="#tsb-03-content">Open</a></td></tr>
          <tr><td>SB-4421</td><td>Sam Rivera</td><td>Ready to ship</td><td><a class="inline-flex items-center min-h-11 px-2.5 text-[0.88rem] font-semibold text-tsb03-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:outline-offset-2" href="#tsb-03-content">Open</a></td></tr>
          <tr><td>SB-4423</td><td>Ada Okafor</td><td>Packing</td><td><a class="inline-flex items-center min-h-11 px-2.5 text-[0.88rem] font-semibold text-tsb03-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:outline-offset-2" href="#tsb-03-content">Open</a></td></tr>
          <tr><td>SB-4425</td><td>Tomas Vidal</td><td>Awaiting stock</td><td><a class="inline-flex items-center min-h-11 px-2.5 text-[0.88rem] font-semibold text-tsb03-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:outline-offset-2" href="#tsb-03-content">Open</a></td></tr>
        </tbody>
      </table>
    </div>

    <div class="absolute inset-0 z-20 bg-[oklch(25%_0.02_60/0.42)] [&[hidden]]:hidden" data-tsb-03-scrim hidden></div>

    <aside class="absolute inset-y-0 right-0 z-30 flex flex-col w-[min(18rem,84%)] bg-tsb03-surf border-l border-tsb03-line translate-x-full transition-transform duration-[220ms] ease-out group-data-open:translate-x-0 motion-reduce:duration-[1ms] [&[hidden]]:hidden" id="tsb-03-drawer" role="dialog" aria-modal="true" aria-labelledby="tsb-03-dh" hidden>
      <header class="flex items-center gap-2 px-3 py-2.5 border-b border-tsb03-line">
        <h3 class="mr-auto m-0 font-tsb03-display text-[1.1rem]" id="tsb-03-dh">Filters</h3>
        <button class="grid place-items-center w-11 h-11 border border-tsb03-line bg-transparent text-tsb03-ink cursor-pointer [&>svg]:w-4 [&>svg]:h-4 focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:-outline-offset-2" type="button" data-tsb-03-close>
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6L6 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
          <span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Close filters</span>
        </button>
      </header>
      <nav class="flex-1 min-h-0 overflow-y-auto [overscroll-behavior:contain]" aria-label="Order views">
        <ul class="list-none m-0 p-0">
          <li><a class="flex items-center min-w-0 min-h-11 px-3 py-2 text-[0.9rem] text-tsb03-ink no-underline border-b border-tsb03-line hover:bg-tsb03-surf2 focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb03-accsoft aria-[current=page]:text-tsb03-acc aria-[current=page]:font-semibold" href="#tsb-03-content" aria-current="page">All open orders</a></li>
          <li><a class="flex items-center min-w-0 min-h-11 px-3 py-2 text-[0.9rem] text-tsb03-ink no-underline border-b border-tsb03-line hover:bg-tsb03-surf2 focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:-outline-offset-2" href="#tsb-03-content">Awaiting stock</a></li>
          <li><a class="flex items-center min-w-0 min-h-11 px-3 py-2 text-[0.9rem] text-tsb03-ink no-underline border-b border-tsb03-line hover:bg-tsb03-surf2 focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:-outline-offset-2" href="#tsb-03-content">Ready to ship</a></li>
          <li><a class="flex items-center min-w-0 min-h-11 px-3 py-2 text-[0.9rem] text-tsb03-ink no-underline border-b border-tsb03-line hover:bg-tsb03-surf2 focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:-outline-offset-2" href="#tsb-03-content">Returns queue</a></li>
          <li><a class="flex items-center min-w-0 min-h-11 px-3 py-2 text-[0.9rem] text-tsb03-ink no-underline border-b border-tsb03-line hover:bg-tsb03-surf2 focus-visible:outline-2 focus-visible:outline-tsb03-acc focus-visible:-outline-offset-2" href="#tsb-03-content">Archived</a></li>
        </ul>
      </nav>
    </aside>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tsb03-bg: #f6f1e8;
  --color-tsb03-bg: oklch(95% 0.018 85);
  --color-tsb03-surf: #fffdf9;
  --color-tsb03-surf: oklch(99% 0.008 85);
  --color-tsb03-surf2: #efe7da;
  --color-tsb03-surf2: oklch(92% 0.022 80);
  --color-tsb03-line: #ddd1bd;
  --color-tsb03-line: oklch(85% 0.032 80);
  --color-tsb03-ink: #23201a;
  --color-tsb03-ink: oklch(24% 0.012 70);
  --color-tsb03-mut: #6d6455;
  --color-tsb03-mut: oklch(48% 0.02 75);
  --color-tsb03-acc: #8a4b1f;
  --color-tsb03-acc: oklch(48% 0.12 52);
  --color-tsb03-accsoft: #f2e4d5;
  --color-tsb03-accsoft: oklch(92% 0.03 60);
  --font-tsb03-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-tsb03-display: 'Source Serif 4', 'Iowan Old Style', Palatino, Georgia, serif;
}
/* 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. */

.tsb-03 {
  width: 100%;
  min-height: 100svh;
}

.tsb-03 *,
.tsb-03 *::before,
.tsb-03 *::after {
  box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */

.tsb-03:has(.tsb-03-theme-input:checked) {
  --color-tsb03-bg: #17140f;
  --color-tsb03-surf: #1f1b15;
  --color-tsb03-surf2: #292319;
  --color-tsb03-line: #3a3226;
  --color-tsb03-ink: #f2ece1;
  --color-tsb03-mut: #a89b87;
  --color-tsb03-acc: #e6a06a;
  --color-tsb03-acc: oklch(78% 0.11 55);
  --color-tsb03-accsoft: #35291d;
}

@media (prefers-color-scheme: dark) {
  .tsb-03 {
    --color-tsb03-bg: #17140f;
    --color-tsb03-surf: #1f1b15;
    --color-tsb03-surf2: #292319;
    --color-tsb03-line: #3a3226;
    --color-tsb03-ink: #f2ece1;
    --color-tsb03-mut: #a89b87;
    --color-tsb03-acc: #e6a06a;
    --color-tsb03-acc: oklch(78% 0.11 55);
    --color-tsb03-accsoft: #35291d;
  }

  .tsb-03:has(.tsb-03-theme-input:checked) {
    --color-tsb03-bg: #f6f1e8;
    --color-tsb03-surf: #fffdf9;
    --color-tsb03-surf2: #efe7da;
    --color-tsb03-line: #ddd1bd;
    --color-tsb03-ink: #23201a;
    --color-tsb03-mut: #6d6455;
    --color-tsb03-acc: #8a4b1f;
    --color-tsb03-acc: oklch(48% 0.12 52);
    --color-tsb03-accsoft: #f2e4d5;
  }
}

@media (prefers-color-scheme: light) {
  .tsb-03:has(.tsb-03-theme-input:checked) {
    --color-tsb03-bg: #17140f;
    --color-tsb03-surf: #1f1b15;
    --color-tsb03-surf2: #292319;
    --color-tsb03-line: #3a3226;
    --color-tsb03-ink: #f2ece1;
    --color-tsb03-mut: #a89b87;
    --color-tsb03-acc: #e6a06a;
    --color-tsb03-acc: oklch(78% 0.11 55);
    --color-tsb03-accsoft: #35291d;
  }
}
```

## JavaScript
```js
(function () {
  document.querySelectorAll('.tsb-03').forEach(function (root) {
    if (root.dataset.bound) return; root.dataset.bound = '1';
    var stage = root.querySelector('.group'),
        drawer = root.querySelector('#tsb-03-drawer'),
        scrim = root.querySelector('[data-tsb-03-scrim]'),
        content = root.querySelector('#tsb-03-content'),
        openBtn = root.querySelector('#tsb-03-open'),
        supported = 'inert' in HTMLElement.prototype,
        returnTo = null;
    function behind() { return Array.prototype.slice.call(content.querySelectorAll('a[href], button:not([disabled])')); }
    function block(on) {
      if (supported) { content.inert = on; return; }
      content.setAttribute('aria-hidden', String(on));
      behind().forEach(function (el) { on ? el.setAttribute('tabindex', '-1') : el.removeAttribute('tabindex'); });
    }
    function onKey(e) { if (e.key === 'Escape') setOpen(false); }
    function setOpen(next) {
      if (next) returnTo = document.activeElement;
      drawer.hidden = scrim.hidden = !next;
      stage.toggleAttribute('data-open', next);
      openBtn.setAttribute('aria-expanded', String(next));
      block(next);
      if (next) {
        document.addEventListener('keydown', onKey);
        requestAnimationFrame(function () { drawer.querySelector('[data-tsb-03-close]').focus(); });
      } else {
        document.removeEventListener('keydown', onKey);
        (returnTo || openBtn).focus({ preventScroll: true });
      }
    }
    openBtn.addEventListener('click', function () { setOpen(true); });
    scrim.addEventListener('click', function () { setOpen(false); });
    root.querySelectorAll('[data-tsb-03-close]').forEach(function (b) { b.addEventListener('click', function () { setOpen(false); }); });
    root.__tsbCleanup = function () { document.removeEventListener('keydown', onKey); };
  });
})();
```

How this works

One attribute does four jobs. inert on the background makes it unfocusable, unclickable, unreachable by a screen-reader virtual cursor and unsearchable by find-in-page. That is the part a Tab trap cannot do, and it is why demos 02 and 03 are a pair rather than alternatives.

The styling of the off state moves onto the element. The CSS version writes .content[inert] { opacity: .45 } in the stylesheet. Here:

<div class="min-w-0 transition-opacity duration-200
     inert:opacity-45" id="tsb-03-content">

so reading the markup tells you what happens when the drawer opens, without going looking for the rule.

The fallback stays in script, because it is behaviour. Where inert is unsupported the script sets aria-hidden and tabindex="-1" on every focusable behind — and the inert: variant simply does not match, so the dimming is skipped, which is the right failure: no dimmed-but-still-clickable state.

Right-edge drawers are a sign flip, nothing more. right-0 plus translate-x-full resting; the focus and inert logic is identical to a left drawer.

Make it yours

  • Dim more or less by editing inert:opacity-45, or drop it entirely — inert still does its work with no visual change at all.
  • Move the drawer to the left edge: right-0 → left-0 and translate-x-full → -translate-x-full.
  • Narrow the panel with w-[min(18rem,84%)], the only width in the demo.
  • Retheme through --color-tsb03-acc in @theme; the button, links, active row and focus rings all read it.
  • Swap the serif by changing --font-tsb03-display and the font <link> in setup.
  • Add a trap on top of this by lifting the keydown block from demo 02; inert and a trap are complementary, not alternatives.

Gotchas — read before shipping

  • inert on an ancestor of the drawer switches the drawer off too. The drawer must be a SIBLING of the inert subtree, never inside it.
  • Do not set aria-hidden and inert together on supporting engines — inert already removes the subtree, and the pair produces duplicate, sometimes contradictory, exposure.
  • The scrim must not live inside the inert content either, or its click handler never fires.
  • inert: matches [inert] and its descendants, so a colour utility under it inherits the dimming; keep the variant on the wrapper and let opacity do the compositing once.
  • Without [&[hidden]]:hidden the closed drawer stays visible, because flex beats the UA [hidden] rule.
  • Reflowing the background while it is inert (a layout shift, a lazy image) still moves content under the user's cursor. Inert is about interaction, not about freezing layout.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

inert is Baseline 2023 (Chrome 102, Safari 15.5, Firefox 112) and Tailwind v4's own floor is higher, so on any engine that can run the generated sheet the attribute is available. The script still feature-detects and falls back to aria-hidden plus tabindex=&quot;-1&quot;.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…