28 CSS Hamburger Menus04 / 28

CSS + JSMIT licensed

Slide-In Off-Canvas Drawer with Dimmed Backdrop Overlay

The app-drawer everyone means when they say 'hamburger menu': a right-hand panel that slides in on translate (never left/right offsets), over a scrim that dims AND blurs the page behind it. The scrim is a real button — so click-outside-to-close is also keyboard- and screen-reader-closable for free — and Escape backs you out. This is the Material/iOS drawer distilled to ~60 lines of CSS.

Published

Live Demo
Try it

The code

<section class="chm-04" aria-label="Off-canvas drawer with dimmed backdrop demo">
  <div class="chm-04__stage">
    <header class="chm-04__bar">
      <span class="chm-04__brand"><i aria-hidden="true"></i>Northwind</span>
      <button class="chm-04__btn" type="button" aria-expanded="false" aria-controls="chm04-drawer" aria-label="Open menu">
        <span aria-hidden="true"><i></i><i></i><i></i></span>
      </button>
    </header>
    <div class="chm-04__content" aria-hidden="true">
      <div class="chm-04__card"></div>
      <div class="chm-04__row"><span></span><span></span></div>
      <div class="chm-04__ln" style="--w:88%"></div>
      <div class="chm-04__ln" style="--w:70%"></div>
      <div class="chm-04__ln" style="--w:79%"></div>
    </div>
    <button class="chm-04__scrim" type="button" tabindex="-1" aria-hidden="true"></button>
    <aside class="chm-04__drawer" id="chm04-drawer" aria-label="Menu">
      <header class="chm-04__dhead">
        <b>Menu</b>
        <button class="chm-04__close" type="button" aria-label="Close menu">
          <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 class="chm-04__nav" aria-label="Primary">
        <a href="#home" style="--i:0" aria-current="page"><svg viewBox="0 0 24 24" 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"><svg viewBox="0 0 24 24" 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>3</b></a>
        <a href="#saved" style="--i:2"><svg viewBox="0 0 24 24" 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"><svg viewBox="0 0 24 24" 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"><svg viewBox="0 0 24 24" 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="chm-04__dfoot"><button type="button">Sign out</button></footer>
    </aside>
  </div>
</section>
.chm-04,
.chm-04 *,
.chm-04 *::before,
.chm-04 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.chm-04 {
  background: var(--bg);
  width: 100%;
  min-height: 100vh;
  box-sizing: border-box;
  --bg: oklch(0.98 0.004 80);
  --panel: oklch(1 0 0);
  --edge: oklch(0.9 0.008 80);
  --ink: oklch(0.25 0.015 60);
  --mut: oklch(0.55 0.015 60);
  --acc: oklch(0.62 0.15 40);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
  display: grid;
  justify-content: center;
}

.chm-04__stage {
  position: relative;
  width: min(400px,88cqi,88vw);
  height: 540px;
  border-radius: 24px;
  overflow: hidden;
  background: var(--bg);
  border: 1px solid var(--edge);
  box-shadow: 0 30px 60px -35px oklch(0.35 0.04 50/.4);
}

.chm-04__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 68px;
  padding: 0 14px 0 22px;
  background: var(--panel);
  border-bottom: 1px solid var(--edge);
}

.chm-04__brand {
  display: flex;
  align-items: center;
  gap: 9px;
  font-weight: 700;
  font-size: 15px;
}

.chm-04__brand i {
  width: 19px;
  height: 19px;
  border-radius: 6px;
  background: conic-gradient(from 200deg,var(--acc),oklch(0.75 0.12 85));
}

.chm-04__btn {
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 12px;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: background .2s;
}

.chm-04__btn:hover {
  background: oklch(0.95 0.008 80);
}

.chm-04__btn:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

.chm-04__btn span {
  position: relative;
  width: 20px;
  height: 14px;
  display: block;
}

.chm-04__btn i {
  position: absolute;
  right: 0;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: width .25s;
}

.chm-04__btn i:nth-child(1) {
  top: 0;
  width: 20px;
}

.chm-04__btn i:nth-child(2) {
  top: 6px;
  width: 14px;
}

.chm-04__btn i:nth-child(3) {
  top: 12px;
  width: 20px;
}

.chm-04__btn:hover i {
  width: 20px;
}

.chm-04__content {
  padding: 20px 22px;
  display: grid;
  gap: 14px;
}

.chm-04__card {
  height: 150px;
  border-radius: 16px;
  background: linear-gradient(140deg,oklch(0.92 0.03 60),oklch(0.88 0.05 35));
}

.chm-04__row {
  display: flex;
  gap: 12px;
}

.chm-04__row span {
  flex: 1;
  height: 72px;
  border-radius: 13px;
  background: var(--panel);
  border: 1px solid var(--edge);
}

.chm-04__ln {
  height: 11px;
  width: var(--w);
  border-radius: 99px;
  background: oklch(0.91 0.006 80);
}

.chm-04__scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  border: none;
  padding: 0;
  background: oklch(0.25 0.02 50/.45);
  backdrop-filter: blur(3px);
  opacity: 0;
  visibility: hidden;
  cursor: pointer;
  transition: opacity .35s,visibility 0s .35s;
}

[data-open] .chm-04__scrim {
  opacity: 1;
  visibility: visible;
  transition: opacity .35s,visibility 0s 0s;
}

.chm-04__drawer {
  position: absolute;
  inset: 0 0 0 auto;
  z-index: 3;
  width: min(84%,320px);
  display: flex;
  flex-direction: column;
  background: var(--panel);
  box-shadow: -24px 0 48px -24px oklch(0.25 0.03 50/.5);
  translate: 100% 0;
  visibility: hidden;
  transition: translate .4s cubic-bezier(.32,.72,.28,1),visibility 0s .4s;
}

[data-open] .chm-04__drawer {
  translate: 0 0;
  visibility: visible;
  transition: translate .4s cubic-bezier(.32,.72,.28,1),visibility 0s 0s;
}

.chm-04__dhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 14px 14px 22px;
}

.chm-04__dhead b {
  font-size: 16px;
  letter-spacing: -.01em;
}

.chm-04__close {
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border: 1px solid var(--edge);
  border-radius: 11px;
  background: var(--panel);
  color: var(--ink);
  cursor: pointer;
  transition: background .2s,rotate .3s;
}

.chm-04__close:hover {
  background: oklch(0.96 0.006 80);
  rotate: 90deg;
}

.chm-04__close:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

.chm-04__nav {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 6px 12px;
  overflow: auto;
  overscroll-behavior: contain;
}

.chm-04__nav a {
  display: flex;
  align-items: center;
  gap: 13px;
  min-height: 48px;
  padding: 0 12px;
  border-radius: 12px;
  text-decoration: none;
  font-size: 14.5px;
  font-weight: 600;
  color: var(--mut);
  opacity: 0;
  translate: 16px 0;
  transition: background .15s,color .15s,opacity .3s,translate .35s cubic-bezier(.2,.7,.2,1);
}

[data-open] .chm-04__nav a {
  opacity: 1;
  translate: 0 0;
  transition-delay: 0s,0s,calc(.12s + var(--i)*.045s),calc(.12s + var(--i)*.045s);
}

.chm-04__nav a svg {
  width: 19px;
  height: 19px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: none;
}

.chm-04__nav a:hover {
  background: oklch(0.965 0.006 80);
  color: var(--ink);
}

.chm-04__nav a:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: -2px;
  color: var(--ink);
}

.chm-04__nav a[aria-current="page"] {
  color: var(--acc);
  background: color-mix(in oklch,var(--acc) 9%,transparent);
}

.chm-04__nav a b {
  margin-left: auto;
  font-size: 11px;
  font-weight: 700;
  color: oklch(1 0 0);
  background: var(--acc);
  border-radius: 99px;
  padding: 2.5px 8px;
}

.chm-04__dfoot {
  margin-top: auto;
  padding: 16px 22px;
  border-top: 1px solid var(--edge);
}

.chm-04__dfoot button {
  width: 100%;
  min-height: 44px;
  border: 1px solid var(--edge);
  border-radius: 12px;
  background: var(--bg);
  font: 600 13.5px 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
  cursor: pointer;
  transition: border-color .2s;
}

.chm-04__dfoot button:hover {
  border-color: oklch(0.75 0.02 60);
}

.chm-04__dfoot button:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .chm-04 * {
    transition-duration: .01s !important;
    transition-delay: 0s !important;
  }
}
(function () {
  document.querySelectorAll('.chm-04__stage').forEach(function (stage) {
    if (stage.dataset.bound) return; stage.dataset.bound = '1';
    var btn = stage.querySelector('.chm-04__btn');
    function set(open) {
      stage.toggleAttribute('data-open', open);
      btn.setAttribute('aria-expanded', String(open));
      if (open) stage.querySelector('.chm-04__close').focus();
      else btn.focus();
    }
    btn.addEventListener('click', function () { set(true); });
    stage.querySelector('.chm-04__scrim').addEventListener('click', function () { set(false); });
    stage.querySelector('.chm-04__close').addEventListener('click', function () { set(false); });
    stage.addEventListener('keydown', function (e) {
      if (e.key === 'Escape' && stage.hasAttribute('data-open')) set(false);
    });
  });
})();
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 Hamburger Menu 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: Slide-In Off-Canvas Drawer with Dimmed Backdrop Overlay
Source: https://codefronts.com/navigation/css-hamburger-menus/slide-in-off-canvas-drawer-with-dimmed-backdrop-overlay/

The app-drawer everyone means when they say 'hamburger menu': a right-hand panel that slides in on translate (never left/right offsets), over a scrim that dims AND blurs the page behind it. The scrim is a real button — so click-outside-to-close is also keyboard- and screen-reader-closable for free — and Escape backs you out. This is the Material/iOS drawer distilled to ~60 lines of CSS.
## HTML
```html
<section class="chm-04" aria-label="Off-canvas drawer with dimmed backdrop demo">
  <div class="chm-04__stage">
    <header class="chm-04__bar">
      <span class="chm-04__brand"><i aria-hidden="true"></i>Northwind</span>
      <button class="chm-04__btn" type="button" aria-expanded="false" aria-controls="chm04-drawer" aria-label="Open menu">
        <span aria-hidden="true"><i></i><i></i><i></i></span>
      </button>
    </header>
    <div class="chm-04__content" aria-hidden="true">
      <div class="chm-04__card"></div>
      <div class="chm-04__row"><span></span><span></span></div>
      <div class="chm-04__ln" style="--w:88%"></div>
      <div class="chm-04__ln" style="--w:70%"></div>
      <div class="chm-04__ln" style="--w:79%"></div>
    </div>
    <button class="chm-04__scrim" type="button" tabindex="-1" aria-hidden="true"></button>
    <aside class="chm-04__drawer" id="chm04-drawer" aria-label="Menu">
      <header class="chm-04__dhead">
        <b>Menu</b>
        <button class="chm-04__close" type="button" aria-label="Close menu">
          <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 class="chm-04__nav" aria-label="Primary">
        <a href="#home" style="--i:0" aria-current="page"><svg viewBox="0 0 24 24" 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"><svg viewBox="0 0 24 24" 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>3</b></a>
        <a href="#saved" style="--i:2"><svg viewBox="0 0 24 24" 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"><svg viewBox="0 0 24 24" 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"><svg viewBox="0 0 24 24" 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="chm-04__dfoot"><button type="button">Sign out</button></footer>
    </aside>
  </div>
</section>
```
## CSS
```css
.chm-04,
.chm-04 *,
.chm-04 *::before,
.chm-04 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.chm-04 {
  background: var(--bg);
  width: 100%;
  min-height: 100vh;
  box-sizing: border-box;
  --bg: oklch(0.98 0.004 80);
  --panel: oklch(1 0 0);
  --edge: oklch(0.9 0.008 80);
  --ink: oklch(0.25 0.015 60);
  --mut: oklch(0.55 0.015 60);
  --acc: oklch(0.62 0.15 40);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
  display: grid;
  justify-content: center;
}

.chm-04__stage {
  position: relative;
  width: min(400px,88cqi,88vw);
  height: 540px;
  border-radius: 24px;
  overflow: hidden;
  background: var(--bg);
  border: 1px solid var(--edge);
  box-shadow: 0 30px 60px -35px oklch(0.35 0.04 50/.4);
}

.chm-04__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 68px;
  padding: 0 14px 0 22px;
  background: var(--panel);
  border-bottom: 1px solid var(--edge);
}

.chm-04__brand {
  display: flex;
  align-items: center;
  gap: 9px;
  font-weight: 700;
  font-size: 15px;
}

.chm-04__brand i {
  width: 19px;
  height: 19px;
  border-radius: 6px;
  background: conic-gradient(from 200deg,var(--acc),oklch(0.75 0.12 85));
}

.chm-04__btn {
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 12px;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: background .2s;
}

.chm-04__btn:hover {
  background: oklch(0.95 0.008 80);
}

.chm-04__btn:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

.chm-04__btn span {
  position: relative;
  width: 20px;
  height: 14px;
  display: block;
}

.chm-04__btn i {
  position: absolute;
  right: 0;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: width .25s;
}

.chm-04__btn i:nth-child(1) {
  top: 0;
  width: 20px;
}

.chm-04__btn i:nth-child(2) {
  top: 6px;
  width: 14px;
}

.chm-04__btn i:nth-child(3) {
  top: 12px;
  width: 20px;
}

.chm-04__btn:hover i {
  width: 20px;
}

.chm-04__content {
  padding: 20px 22px;
  display: grid;
  gap: 14px;
}

.chm-04__card {
  height: 150px;
  border-radius: 16px;
  background: linear-gradient(140deg,oklch(0.92 0.03 60),oklch(0.88 0.05 35));
}

.chm-04__row {
  display: flex;
  gap: 12px;
}

.chm-04__row span {
  flex: 1;
  height: 72px;
  border-radius: 13px;
  background: var(--panel);
  border: 1px solid var(--edge);
}

.chm-04__ln {
  height: 11px;
  width: var(--w);
  border-radius: 99px;
  background: oklch(0.91 0.006 80);
}

.chm-04__scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  border: none;
  padding: 0;
  background: oklch(0.25 0.02 50/.45);
  backdrop-filter: blur(3px);
  opacity: 0;
  visibility: hidden;
  cursor: pointer;
  transition: opacity .35s,visibility 0s .35s;
}

[data-open] .chm-04__scrim {
  opacity: 1;
  visibility: visible;
  transition: opacity .35s,visibility 0s 0s;
}

.chm-04__drawer {
  position: absolute;
  inset: 0 0 0 auto;
  z-index: 3;
  width: min(84%,320px);
  display: flex;
  flex-direction: column;
  background: var(--panel);
  box-shadow: -24px 0 48px -24px oklch(0.25 0.03 50/.5);
  translate: 100% 0;
  visibility: hidden;
  transition: translate .4s cubic-bezier(.32,.72,.28,1),visibility 0s .4s;
}

[data-open] .chm-04__drawer {
  translate: 0 0;
  visibility: visible;
  transition: translate .4s cubic-bezier(.32,.72,.28,1),visibility 0s 0s;
}

.chm-04__dhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 14px 14px 22px;
}

.chm-04__dhead b {
  font-size: 16px;
  letter-spacing: -.01em;
}

.chm-04__close {
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border: 1px solid var(--edge);
  border-radius: 11px;
  background: var(--panel);
  color: var(--ink);
  cursor: pointer;
  transition: background .2s,rotate .3s;
}

.chm-04__close:hover {
  background: oklch(0.96 0.006 80);
  rotate: 90deg;
}

.chm-04__close:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

.chm-04__nav {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 6px 12px;
  overflow: auto;
  overscroll-behavior: contain;
}

.chm-04__nav a {
  display: flex;
  align-items: center;
  gap: 13px;
  min-height: 48px;
  padding: 0 12px;
  border-radius: 12px;
  text-decoration: none;
  font-size: 14.5px;
  font-weight: 600;
  color: var(--mut);
  opacity: 0;
  translate: 16px 0;
  transition: background .15s,color .15s,opacity .3s,translate .35s cubic-bezier(.2,.7,.2,1);
}

[data-open] .chm-04__nav a {
  opacity: 1;
  translate: 0 0;
  transition-delay: 0s,0s,calc(.12s + var(--i)*.045s),calc(.12s + var(--i)*.045s);
}

.chm-04__nav a svg {
  width: 19px;
  height: 19px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: none;
}

.chm-04__nav a:hover {
  background: oklch(0.965 0.006 80);
  color: var(--ink);
}

.chm-04__nav a:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: -2px;
  color: var(--ink);
}

.chm-04__nav a[aria-current="page"] {
  color: var(--acc);
  background: color-mix(in oklch,var(--acc) 9%,transparent);
}

.chm-04__nav a b {
  margin-left: auto;
  font-size: 11px;
  font-weight: 700;
  color: oklch(1 0 0);
  background: var(--acc);
  border-radius: 99px;
  padding: 2.5px 8px;
}

.chm-04__dfoot {
  margin-top: auto;
  padding: 16px 22px;
  border-top: 1px solid var(--edge);
}

.chm-04__dfoot button {
  width: 100%;
  min-height: 44px;
  border: 1px solid var(--edge);
  border-radius: 12px;
  background: var(--bg);
  font: 600 13.5px 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
  cursor: pointer;
  transition: border-color .2s;
}

.chm-04__dfoot button:hover {
  border-color: oklch(0.75 0.02 60);
}

.chm-04__dfoot button:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .chm-04 * {
    transition-duration: .01s !important;
    transition-delay: 0s !important;
  }
}
```

## JavaScript
```js
(function () {
  document.querySelectorAll('.chm-04__stage').forEach(function (stage) {
    if (stage.dataset.bound) return; stage.dataset.bound = '1';
    var btn = stage.querySelector('.chm-04__btn');
    function set(open) {
      stage.toggleAttribute('data-open', open);
      btn.setAttribute('aria-expanded', String(open));
      if (open) stage.querySelector('.chm-04__close').focus();
      else btn.focus();
    }
    btn.addEventListener('click', function () { set(true); });
    stage.querySelector('.chm-04__scrim').addEventListener('click', function () { set(false); });
    stage.querySelector('.chm-04__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, strict order: content, scrim, drawer. Both moving parts key off one data-open attribute on the stage:

.scrim{ position:absolute; inset:0; opacity:0; visibility:hidden;
  background:oklch(0.2 0.02 260 / .45); backdrop-filter:blur(3px);
  transition:opacity .35s, visibility 0s .35s; }
.drawer{ position:absolute; inset:0 0 0 auto; width:min(84%, 320px);
  translate:100% 0;
  transition:translate .4s cubic-bezier(.32,.72,.28,1), visibility 0s .4s;
  visibility:hidden; }
[data-open] .scrim,[data-open] .drawer{
  opacity:1; translate:0 0; visibility:visible; transition-delay:0s; }

Why translate and not right:-320px? Offsets are layout properties — animating them reflows every frame and stutters on mid-range phones. A transform moves a finished paint on the compositor. The visibility pairing (delayed on close, immediate on open) keeps the closed drawer out of the Tab order without display:none killing the transition.

The scrim is the pattern's quiet accessibility win:

<button class="scrim" tabindex="-1" aria-hidden="true"></button>

As a button it takes the click that means "close" — no document-level click listeners, no bubbling edge cases. It's aria-hidden + tabindex="-1" because Escape and the drawer's × already serve keyboard and AT users; the scrim is a pointer affordance. Easing matters here: cubic-bezier(.32,.72,.28,1) is the iOS sheet curve — fast launch, long gentle landing. Symmetric ease-in-out drawers feel rubbery.

Make it yours

  • Drawer side: swap inset:0 0 0 auto + translate:100% 0 for inset:0 auto 0 0 + translate:-100% 0 — done, left drawer.
  • Width formula min(84%, 320px): the 84% guarantees a visible sliver of page (the user's map back out), the 320px caps it on tablets.
  • Scrim recipe: darkness in the oklch alpha (.45), material in the backdrop-filter. Drop the blur for a flat dim; raise to 8px for heavy glass.
  • Production: change the three position:absolute to fixed, delete the stage height, and add overflow:hidden to body while open (one JS line) to stop background scroll.

Gotchas — read before shipping

  • Never animate left/right for drawers — layout-thrashing jank is THE most common off-canvas bug. transform/translate only.
  • Without delayed visibility (or inert) the closed drawer's links remain keyboard-reachable off-screen — an instant WCAG failure and a real-world confusion for screen-reader users.
  • backdrop-filter on the scrim needs the scrim to be its own element, not a ::before on 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 also lock body scroll; the docs' one-liner (document.body.style.overflow) or overscroll-behavior:contain on the drawer's own scroller handles it.

Browser support

ChromeSafariFirefoxEdge
111+16.4+113+111+

translate + visibility transitions are ancient; backdrop-filter is universal since Firefox 103. The floor is the oklch()/color-mix() palette — with hex colors this pattern runs back to 2015 browsers (as transform: translateX).

Techniques used in this demo

Search CodeFronts

Loading…