28 CSS Hamburger Menus17 / 28

CSS + JSMIT licensed

WCAG Accessible Hamburger Menu HTML CSS JS Example

The audit-proof build: a menu drawer implemented as a true modal dialog — role=dialog, aria-modal, focus moved in on open, trapped while open, restored on close, background inert, Escape wired, 44px targets, visible focus rings, reduced-motion path. Every requirement is tied to its WCAG success criterion in the docs, so this doubles as the checklist you hand to a reviewer. This is the pattern demo 02's disclosure graduates to when the menu covers the page.

Published Updated

Live Demo
Try it

The code

<section class="chm-17" aria-label="WCAG accessible hamburger menu demo">
  <div class="chm-17__stage">
    <div class="chm-17__page" id="chm17-page">
      <header class="chm-17__bar">
        <span class="chm-17__brand"><i aria-hidden="true"></i>CivicWorks</span>
        <button class="chm-17__btn" type="button" aria-expanded="false" aria-controls="chm17-menu" aria-label="Open navigation">
          <span class="chm-17__ico" aria-hidden="true"><i></i><i></i><i></i></span>
          <span class="chm-17__btntext" aria-hidden="true">Menu</span>
        </button>
      </header>
      <div class="chm-17__content" aria-hidden="true">
        <div class="chm-17__badge">WCAG 2.2 AA</div>
        <div class="chm-17__ln" style="--w:84%"></div>
        <div class="chm-17__ln" style="--w:66%"></div>
        <div class="chm-17__ln" style="--w:75%"></div>
      </div>
    </div>
    <div class="chm-17__scrim" aria-hidden="true"></div>
    <aside class="chm-17__menu" id="chm17-menu" role="dialog" aria-modal="true" aria-labelledby="chm17-title" tabindex="-1">
      <header class="chm-17__mhead">
        <b id="chm17-title">Navigation</b>
        <button class="chm-17__close" type="button" aria-label="Close navigation"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button>
      </header>
      <nav aria-label="Primary">
        <ul class="chm-17__list">
          <li><a href="#services" aria-current="page">Services</a></li>
          <li><a href="#permits">Permits &amp; licences</a></li>
          <li><a href="#payments">Payments</a></li>
          <li><a href="#accessibility">Accessibility statement</a></li>
          <li><a href="#contact">Contact us</a></li>
        </ul>
      </nav>
      <p class="chm-17__hint" aria-hidden="true"><kbd>Tab</kbd> cycles inside · <kbd>Esc</kbd> closes &amp; restores focus · page behind is <code>inert</code></p>
    </aside>
    <p class="chm-17__live" role="status" aria-live="polite"></p>
  </div>
</section>
.chm-17,
.chm-17 *,
.chm-17 *::before,
.chm-17 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.chm-17 {
  background: var(--bg);
  width: 100%;
  min-height: 100vh;
  box-sizing: border-box;
  --target: 44px;
  --bg: oklch(0.98 0.004 250);
  --panel: oklch(1 0 0);
  --edge: oklch(0.87 0.01 250);
  --ink: oklch(0.22 0.02 255);
  --mut: oklch(0.45 0.018 255);
  --acc: oklch(0.48 0.16 258);
  --focus: oklch(0.55 0.2 262);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
  display: grid;
  justify-content: center;
}

.chm-17__stage {
  position: relative;
  width: min(420px,88cqi,88vw);
  height: 540px;
  border-radius: 24px;
  overflow: hidden;
  background: var(--bg);
  border: 1px solid var(--edge);
}

.chm-17__page {
  height: 100%;
}

.chm-17__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-17__brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 700;
  font-size: 15.5px;
}

.chm-17__brand i {
  width: 20px;
  height: 20px;
  border-radius: 6px;
  background: var(--acc);
}

.chm-17__btn {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: var(--target);
  min-width: var(--target);
  padding: 0 14px;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: var(--panel);
  color: var(--ink);
  font: 700 13.5px 'Segoe UI',system-ui,sans-serif;
  cursor: pointer;
  transition: background .2s,color .2s;
}

.chm-17__btn:hover {
  background: var(--ink);
  color: var(--panel);
}

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

.chm-17__ico {
  position: relative;
  width: 17px;
  height: 13px;
  display: block;
}

.chm-17__ico i {
  position: absolute;
  left: 0;
  width: 17px;
  height: 2.5px;
  border-radius: 2px;
  background: currentColor;
}

.chm-17__ico i:nth-child(1) {
  top: 0;
}

.chm-17__ico i:nth-child(2) {
  top: 5.25px;
}

.chm-17__ico i:nth-child(3) {
  top: 10.5px;
}

.chm-17__content {
  padding: 22px;
  display: grid;
  gap: 14px;
  justify-items: start;
}

.chm-17__badge {
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .08em;
  color: oklch(1 0 0);
  background: oklch(0.5 0.14 155);
  border-radius: 8px;
  padding: 8px 13px;
  margin-bottom: 6px;
}

.chm-17__ln {
  height: 12px;
  width: var(--w);
  border-radius: 99px;
  background: oklch(0.9 0.008 250);
}

.chm-17__scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: oklch(0.2 0.03 258/.5);
  opacity: 0;
  visibility: hidden;
  transition: opacity .3s,visibility 0s .3s;
}

[data-open] .chm-17__scrim {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

.chm-17__menu {
  position: absolute;
  inset: 0 0 0 auto;
  z-index: 3;
  width: min(88%,330px);
  background: var(--panel);
  border-left: 1px solid var(--edge);
  box-shadow: -20px 0 44px -20px oklch(0.2 0.04 258/.5);
  display: flex;
  flex-direction: column;
  translate: 100% 0;
  visibility: hidden;
  transition: translate .38s cubic-bezier(.32,.72,.28,1),visibility 0s .38s;
}

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

.chm-17__mhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 14px 12px 22px;
  border-bottom: 2px solid var(--ink);
}

.chm-17__mhead b {
  font-size: 17px;
}

.chm-17__close {
  width: var(--target);
  height: var(--target);
  display: grid;
  place-items: center;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: var(--panel);
  color: var(--ink);
  cursor: pointer;
  transition: background .2s,color .2s;
}

.chm-17__close:hover {
  background: var(--ink);
  color: var(--panel);
}

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

.chm-17__list {
  list-style: none;
  padding: 10px 12px;
  display: grid;
  gap: 2px;
}

.chm-17__list a {
  display: flex;
  align-items: center;
  min-height: var(--target);
  padding: 0 12px;
  border-radius: 9px;
  font-size: 15.5px;
  font-weight: 600;
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 3px;
  transition: background .15s,text-decoration-color .15s;
}

.chm-17__list a:hover {
  background: oklch(0.95 0.008 255);
  text-decoration-color: var(--acc);
}

.chm-17__list a:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: -3px;
}

.chm-17__list a[aria-current="page"] {
  background: color-mix(in oklch,var(--acc) 10%,transparent);
  color: var(--acc);
  font-weight: 700;
}

.chm-17__hint {
  margin-top: auto;
  padding: 16px 22px;
  font-size: 12px;
  line-height: 1.9;
  color: var(--mut);
  border-top: 1px solid var(--edge);
  text-wrap: pretty;
}

.chm-17__hint kbd {
  font: 700 10.5px ui-monospace,Menlo,Consolas,monospace;
  border: 1.5px solid var(--edge);
  border-bottom-width: 3px;
  border-radius: 5px;
  padding: 1.5px 6px;
  background: var(--bg);
}

.chm-17__hint code {
  font: 11px ui-monospace,Menlo,Consolas,monospace;
  background: oklch(0.93 0.006 250);
  border-radius: 4px;
  padding: 1px 5px;
}

.chm-17__live {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .chm-17 .chm-17__menu {
    translate: 0 0;
    opacity: 0;
    transition: opacity .15s,visibility 0s .15s;
  }

  .chm-17 [data-open] .chm-17__menu {
    opacity: 1;
    transition-delay: 0s;
  }

  .chm-17 * {
    transition-duration: .15s !important;
  }
}
(function () {
  document.querySelectorAll('.chm-17__stage').forEach(function (stage) {
    if (stage.dataset.bound) return; stage.dataset.bound = '1';
    var page = stage.querySelector('.chm-17__page'),
        menu = stage.querySelector('.chm-17__menu'),
        burger = stage.querySelector('.chm-17__btn'),
        closeBtn = stage.querySelector('.chm-17__close'),
        live = stage.querySelector('.chm-17__live'),
        opener = null;
    function focusables() {
      return menu.querySelectorAll('a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
    }
    function set(open) {
      stage.toggleAttribute('data-open', open);
      burger.setAttribute('aria-expanded', String(open));
      burger.setAttribute('aria-label', open ? 'Close navigation' : 'Open navigation');
      if (open) {
        opener = document.activeElement;
        page.setAttribute('inert', '');
        closeBtn.focus();
        live.textContent = 'Navigation opened';
      } else {
        page.removeAttribute('inert');
        (opener || burger).focus();
        live.textContent = 'Navigation closed';
      }
    }
    burger.addEventListener('click', function () { set(true); });
    closeBtn.addEventListener('click', function () { set(false); });
    stage.querySelector('.chm-17__scrim').addEventListener('click', function () { set(false); });
    stage.addEventListener('keydown', function (e) {
      if (!stage.hasAttribute('data-open')) return;
      if (e.key === 'Escape') { set(false); return; }
      if (e.key !== 'Tab') return;
      var f = focusables(), first = f[0], last = f[f.length - 1];
      if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
      else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
    });
  });
})();
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: WCAG Accessible Hamburger Menu HTML CSS JS Example
Source: https://codefronts.com/navigation/css-hamburger-menus/accessible-hamburger-menu-wcag-compliant/

The audit-proof build: a menu drawer implemented as a true modal dialog — role=dialog, aria-modal, focus moved in on open, trapped while open, restored on close, background inert, Escape wired, 44px targets, visible focus rings, reduced-motion path. Every requirement is tied to its WCAG success criterion in the docs, so this doubles as the checklist you hand to a reviewer. This is the pattern demo 02's disclosure graduates to when the menu covers the page.
## HTML
```html
<section class="chm-17" aria-label="WCAG accessible hamburger menu demo">
  <div class="chm-17__stage">
    <div class="chm-17__page" id="chm17-page">
      <header class="chm-17__bar">
        <span class="chm-17__brand"><i aria-hidden="true"></i>CivicWorks</span>
        <button class="chm-17__btn" type="button" aria-expanded="false" aria-controls="chm17-menu" aria-label="Open navigation">
          <span class="chm-17__ico" aria-hidden="true"><i></i><i></i><i></i></span>
          <span class="chm-17__btntext" aria-hidden="true">Menu</span>
        </button>
      </header>
      <div class="chm-17__content" aria-hidden="true">
        <div class="chm-17__badge">WCAG 2.2 AA</div>
        <div class="chm-17__ln" style="--w:84%"></div>
        <div class="chm-17__ln" style="--w:66%"></div>
        <div class="chm-17__ln" style="--w:75%"></div>
      </div>
    </div>
    <div class="chm-17__scrim" aria-hidden="true"></div>
    <aside class="chm-17__menu" id="chm17-menu" role="dialog" aria-modal="true" aria-labelledby="chm17-title" tabindex="-1">
      <header class="chm-17__mhead">
        <b id="chm17-title">Navigation</b>
        <button class="chm-17__close" type="button" aria-label="Close navigation"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button>
      </header>
      <nav aria-label="Primary">
        <ul class="chm-17__list">
          <li><a href="#services" aria-current="page">Services</a></li>
          <li><a href="#permits">Permits &amp; licences</a></li>
          <li><a href="#payments">Payments</a></li>
          <li><a href="#accessibility">Accessibility statement</a></li>
          <li><a href="#contact">Contact us</a></li>
        </ul>
      </nav>
      <p class="chm-17__hint" aria-hidden="true"><kbd>Tab</kbd> cycles inside · <kbd>Esc</kbd> closes &amp; restores focus · page behind is <code>inert</code></p>
    </aside>
    <p class="chm-17__live" role="status" aria-live="polite"></p>
  </div>
</section>
```
## CSS
```css
.chm-17,
.chm-17 *,
.chm-17 *::before,
.chm-17 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.chm-17 {
  background: var(--bg);
  width: 100%;
  min-height: 100vh;
  box-sizing: border-box;
  --target: 44px;
  --bg: oklch(0.98 0.004 250);
  --panel: oklch(1 0 0);
  --edge: oklch(0.87 0.01 250);
  --ink: oklch(0.22 0.02 255);
  --mut: oklch(0.45 0.018 255);
  --acc: oklch(0.48 0.16 258);
  --focus: oklch(0.55 0.2 262);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
  display: grid;
  justify-content: center;
}

.chm-17__stage {
  position: relative;
  width: min(420px,88cqi,88vw);
  height: 540px;
  border-radius: 24px;
  overflow: hidden;
  background: var(--bg);
  border: 1px solid var(--edge);
}

.chm-17__page {
  height: 100%;
}

.chm-17__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-17__brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 700;
  font-size: 15.5px;
}

.chm-17__brand i {
  width: 20px;
  height: 20px;
  border-radius: 6px;
  background: var(--acc);
}

.chm-17__btn {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: var(--target);
  min-width: var(--target);
  padding: 0 14px;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: var(--panel);
  color: var(--ink);
  font: 700 13.5px 'Segoe UI',system-ui,sans-serif;
  cursor: pointer;
  transition: background .2s,color .2s;
}

.chm-17__btn:hover {
  background: var(--ink);
  color: var(--panel);
}

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

.chm-17__ico {
  position: relative;
  width: 17px;
  height: 13px;
  display: block;
}

.chm-17__ico i {
  position: absolute;
  left: 0;
  width: 17px;
  height: 2.5px;
  border-radius: 2px;
  background: currentColor;
}

.chm-17__ico i:nth-child(1) {
  top: 0;
}

.chm-17__ico i:nth-child(2) {
  top: 5.25px;
}

.chm-17__ico i:nth-child(3) {
  top: 10.5px;
}

.chm-17__content {
  padding: 22px;
  display: grid;
  gap: 14px;
  justify-items: start;
}

.chm-17__badge {
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .08em;
  color: oklch(1 0 0);
  background: oklch(0.5 0.14 155);
  border-radius: 8px;
  padding: 8px 13px;
  margin-bottom: 6px;
}

.chm-17__ln {
  height: 12px;
  width: var(--w);
  border-radius: 99px;
  background: oklch(0.9 0.008 250);
}

.chm-17__scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: oklch(0.2 0.03 258/.5);
  opacity: 0;
  visibility: hidden;
  transition: opacity .3s,visibility 0s .3s;
}

[data-open] .chm-17__scrim {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

.chm-17__menu {
  position: absolute;
  inset: 0 0 0 auto;
  z-index: 3;
  width: min(88%,330px);
  background: var(--panel);
  border-left: 1px solid var(--edge);
  box-shadow: -20px 0 44px -20px oklch(0.2 0.04 258/.5);
  display: flex;
  flex-direction: column;
  translate: 100% 0;
  visibility: hidden;
  transition: translate .38s cubic-bezier(.32,.72,.28,1),visibility 0s .38s;
}

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

.chm-17__mhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 14px 12px 22px;
  border-bottom: 2px solid var(--ink);
}

.chm-17__mhead b {
  font-size: 17px;
}

.chm-17__close {
  width: var(--target);
  height: var(--target);
  display: grid;
  place-items: center;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: var(--panel);
  color: var(--ink);
  cursor: pointer;
  transition: background .2s,color .2s;
}

.chm-17__close:hover {
  background: var(--ink);
  color: var(--panel);
}

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

.chm-17__list {
  list-style: none;
  padding: 10px 12px;
  display: grid;
  gap: 2px;
}

.chm-17__list a {
  display: flex;
  align-items: center;
  min-height: var(--target);
  padding: 0 12px;
  border-radius: 9px;
  font-size: 15.5px;
  font-weight: 600;
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 3px;
  transition: background .15s,text-decoration-color .15s;
}

.chm-17__list a:hover {
  background: oklch(0.95 0.008 255);
  text-decoration-color: var(--acc);
}

.chm-17__list a:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: -3px;
}

.chm-17__list a[aria-current="page"] {
  background: color-mix(in oklch,var(--acc) 10%,transparent);
  color: var(--acc);
  font-weight: 700;
}

.chm-17__hint {
  margin-top: auto;
  padding: 16px 22px;
  font-size: 12px;
  line-height: 1.9;
  color: var(--mut);
  border-top: 1px solid var(--edge);
  text-wrap: pretty;
}

.chm-17__hint kbd {
  font: 700 10.5px ui-monospace,Menlo,Consolas,monospace;
  border: 1.5px solid var(--edge);
  border-bottom-width: 3px;
  border-radius: 5px;
  padding: 1.5px 6px;
  background: var(--bg);
}

.chm-17__hint code {
  font: 11px ui-monospace,Menlo,Consolas,monospace;
  background: oklch(0.93 0.006 250);
  border-radius: 4px;
  padding: 1px 5px;
}

.chm-17__live {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .chm-17 .chm-17__menu {
    translate: 0 0;
    opacity: 0;
    transition: opacity .15s,visibility 0s .15s;
  }

  .chm-17 [data-open] .chm-17__menu {
    opacity: 1;
    transition-delay: 0s;
  }

  .chm-17 * {
    transition-duration: .15s !important;
  }
}
```

## JavaScript
```js
(function () {
  document.querySelectorAll('.chm-17__stage').forEach(function (stage) {
    if (stage.dataset.bound) return; stage.dataset.bound = '1';
    var page = stage.querySelector('.chm-17__page'),
        menu = stage.querySelector('.chm-17__menu'),
        burger = stage.querySelector('.chm-17__btn'),
        closeBtn = stage.querySelector('.chm-17__close'),
        live = stage.querySelector('.chm-17__live'),
        opener = null;
    function focusables() {
      return menu.querySelectorAll('a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
    }
    function set(open) {
      stage.toggleAttribute('data-open', open);
      burger.setAttribute('aria-expanded', String(open));
      burger.setAttribute('aria-label', open ? 'Close navigation' : 'Open navigation');
      if (open) {
        opener = document.activeElement;
        page.setAttribute('inert', '');
        closeBtn.focus();
        live.textContent = 'Navigation opened';
      } else {
        page.removeAttribute('inert');
        (opener || burger).focus();
        live.textContent = 'Navigation closed';
      }
    }
    burger.addEventListener('click', function () { set(true); });
    closeBtn.addEventListener('click', function () { set(false); });
    stage.querySelector('.chm-17__scrim').addEventListener('click', function () { set(false); });
    stage.addEventListener('keydown', function (e) {
      if (!stage.hasAttribute('data-open')) return;
      if (e.key === 'Escape') { set(false); return; }
      if (e.key !== 'Tab') return;
      var f = focusables(), first = f[0], last = f[f.length - 1];
      if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
      else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
    });
  });
})();
```

How this works

A fullscreen/overlay menu IS a modal dialog in ARIA terms, and modality is a three-part promise:

<aside role="dialog" aria-modal="true" aria-labelledby="chm17-title">

// 1. Focus moves IN on open (2.4.3 Focus Order)
closeBtn.focus();
// 2. The page behind stops existing (1.3.2 / 2.4.3)
pageRoot.setAttribute('inert', '');
// 3. Focus returns on close (2.4.3 again)
burgerBtn.focus();

inert is the modern trap: one attribute makes the entire background unfocusable AND invisible to screen readers — what needed 50 lines of focus-sentinel code in 2020. The belt-and-braces Tab loop remains (12 lines, first/last element wrap) for the edge cases inert doesn't cover: focus restored from browser UI, or an inert-less ancient engine.

The rest of the checklist, each line load-bearing in an audit: 4.1.2 — the burger is a <button> with aria-expanded + aria-controls and an accessible name that flips (Open/Close navigation). 2.1.1/2.1.2 — everything reachable and leavable by keyboard: Escape closes (dialogs must), Tab cycles inside. 2.4.7/1.4.11 — 2px focus indicators at ≥3:1 against both surfaces. 2.5.8 — every target ≥44px (WCAG 2.2 minimum is 24, mobile guidance is 44). 1.4.3 — link text at 15.5px/600 on this surface is 8.9:1. 2.3.3 — the slide becomes a fade under prefers-reduced-motion. The live region announcing 'Navigation opened' is a courtesy, not a requirement — aria-expanded already announces state — but it demonstrates polite (not assertive) announcements done right.

Make it yours

  • This drawer slides from the right; the ARIA layer is geometry-independent — restyle to fullscreen or bottom-sheet without touching a line of JS.
  • The focusables query (a[href], button, input…) is the trap's definition of 'inside' — extend it if you add tabindex=0 widgets to the menu.
  • To convert to demo 02's lighter disclosure (menu NOT covering content): drop role=dialog/aria-modal/inert/trap, keep everything else. The docs' rule of thumb: covers content = dialog; pushes content = disclosure.
  • Target size: the 44px minimum lives in one --target token; enterprise desktop apps may drop to 32px visual with padding keeping the 24px WCAG 2.2 floor.

Gotchas — read before shipping

  • role="dialog" without moving focus into it is WORSE than no role — screen readers announce a dialog the user isn't in, and 'aria-modal=true' hides the rest of the page from them while their focus is still out there. The role and the focus move ship together, always.
  • Don't inert the element that CONTAINS the dialog — inert is inherited, and an inert dialog is a lockout. Page content and dialog must be sibling subtrees (note the separate #chm17-page wrapper).
  • aria-hidden="true" on the background is the legacy inert substitute — if you use it, remember it does NOT block keyboard focus, only AT; you need both it and a trap. inert does both, prefer it.
  • Restoring focus needs the ELEMENT, not document.activeElement at close time (that's the close button). Capture the opener reference at open time; this demo stores it in a variable, the robust habit.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

inert: Chrome 102 / Safari 15.5 / Firefox 112 — Baseline 2023; the Tab-loop fallback covers anything older. Everything else is evergreen. Test with NVDA+Firefox and VoiceOver+Safari; that pair catches 95% of real issues.

Search CodeFronts

Loading…