Accessibility pattern · Navigation

Mobile navigation drawer

The drawer is a modal dialog: showModal() makes the page behind inert, so focus and the screen reader stay in the menu until it closes. The Menu button keeps its name and reports expanded or collapsed; sections inside the drawer are disclosure buttons, not menus within menus.

WCAG criteria
6
Keyboard rules
4
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Monsoon Times

You are on

Home

This area stands in for the page. Open the menu and choose a link: the drawer closes, the link becomes the current page and focus moves to this heading.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Enter or SpaceOn Menu, opens the drawer and moves focus into it. On a section button inside, shows or hides its links.
Tab or ShiftTabMoves through the drawer's controls only; the page behind cannot be reached while it is open.
EscapeCloses the drawer and returns focus to the Menu button.
EnterOn a link, goes to that page: the drawer closes and focus moves to the new page's heading.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the Menu buttonMenu, button, collapsed
The drawer opensMenu, dialog. Close menu, button
Tab reaches a sectionSections, button, collapsed
Enter opens the sectionExpanded
The drawer closesMenu, button, collapsed (focus is back on the button)

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-mobile-nav" data-ap-mobile-nav>
  <div class="ap-mobile-nav__screen">
    <header class="ap-mobile-nav__bar">
      <a class="ap-mobile-nav__brand" href="#mobnav-home">
        <svg class="ap-mobile-nav__mark" viewBox="0 0 32 32" aria-hidden="true" focusable="false"><circle cx="16" cy="16" r="14"/><path d="M8 20c3-2 5-2 8 0s5 2 8 0M8 14c3-2 5-2 8 0s5 2 8 0"/></svg>
        <span>Monsoon Times</span>
      </a>
      <button type="button" class="ap-mobile-nav__open" data-ap-open aria-haspopup="dialog" aria-expanded="false" aria-controls="mobnav-drawer">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7h16M4 12h16M4 17h16"/></svg>
        <span>Menu</span>
      </button>
    </header>
    <div class="ap-mobile-nav__page">
      <p class="ap-mobile-nav__kicker">You are on</p>
      <h3 class="ap-mobile-nav__page-name" id="mobnav-page" tabindex="-1" data-ap-page>Home</h3>
      <p class="ap-mobile-nav__page-text">This area stands in for the page. Open the menu and choose a link: the drawer closes, the link becomes the current page and focus moves to this heading.</p>
    </div>
  </div>

  <dialog class="ap-mobile-nav__drawer" id="mobnav-drawer" aria-labelledby="mobnav-name">
    <div class="ap-mobile-nav__sheet">
      <div class="ap-mobile-nav__top">
        <h2 class="ap-mobile-nav__name" id="mobnav-name">Menu</h2>
        <button type="button" class="ap-mobile-nav__close" data-ap-close aria-label="Close menu">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18"/></svg>
        </button>
      </div>
      <nav class="ap-mobile-nav__nav" aria-label="Main">
        <ul class="ap-mobile-nav__list">
          <li><a class="ap-mobile-nav__link" href="#mobnav-home" aria-current="page">Home</a></li>
          <li>
            <button type="button" class="ap-mobile-nav__toggle" aria-expanded="false" aria-controls="mobnav-sections">
              Sections
              <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
            </button>
            <ul class="ap-mobile-nav__sub" id="mobnav-sections" hidden>
              <li><a class="ap-mobile-nav__link" href="#mobnav-india">India</a></li>
              <li><a class="ap-mobile-nav__link" href="#mobnav-world">World</a></li>
              <li><a class="ap-mobile-nav__link" href="#mobnav-business">Business</a></li>
              <li><a class="ap-mobile-nav__link" href="#mobnav-sport">Sport</a></li>
              <li><a class="ap-mobile-nav__link" href="#mobnav-culture">Culture</a></li>
            </ul>
          </li>
          <li>
            <button type="button" class="ap-mobile-nav__toggle" aria-expanded="false" aria-controls="mobnav-cities">
              Cities
              <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
            </button>
            <ul class="ap-mobile-nav__sub" id="mobnav-cities" hidden>
              <li><a class="ap-mobile-nav__link" href="#mobnav-mumbai">Mumbai</a></li>
              <li><a class="ap-mobile-nav__link" href="#mobnav-delhi">Delhi</a></li>
              <li><a class="ap-mobile-nav__link" href="#mobnav-bengaluru">Bengaluru</a></li>
              <li><a class="ap-mobile-nav__link" href="#mobnav-chennai">Chennai</a></li>
              <li><a class="ap-mobile-nav__link" href="#mobnav-kolkata">Kolkata</a></li>
            </ul>
          </li>
          <li><a class="ap-mobile-nav__link" href="#mobnav-podcasts">Podcasts</a></li>
          <li><a class="ap-mobile-nav__link" href="#mobnav-newsletters">Newsletters</a></li>
        </ul>
      </nav>
      <div class="ap-mobile-nav__foot">
        <a class="ap-btn ap-btn--primary" href="#mobnav-subscribe">Subscribe</a>
        <a class="ap-btn" href="#mobnav-sign-in">Sign in</a>
      </div>
    </div>
  </dialog>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The drawer holds a nav landmark of lists, and each nested section is a list that follows its button.

  • 2.1.1 Keyboard Level A

    Opening, closing and every section work from the keyboard; Escape is handled by the browser.

  • 2.1.2 No Keyboard Trap Level A

    Focus is held in the drawer while it is open, but Escape, the close button and every link always let it out.

  • 2.4.3 Focus Order Level A

    Focus moves into the drawer when it opens and back to the Menu button when it closes, or to the new page's heading after a link.

  • 2.5.8 Target Size (Minimum) Level AA

    Every row in the drawer is at least 44 pixels high, well clear of the 24-pixel minimum, and the close button is 44 pixels square.

  • 4.1.2 Name, Role, Value Level A

    The Menu button exposes aria-expanded and aria-controls, the drawer is a dialog named Menu, and section buttons carry aria-expanded.

Usage

When to use it

Use it

  • Small screens, where the full navigation does not fit across the header.
  • Sites with two levels of navigation that people browse on phones.

Use something else

  • Wide screens with room for the links: keep them in view, because hidden navigation gets used less.
  • Two or three links: show them in the header and skip the drawer.

Common failures

How it usually goes wrong

  • A drawer that is just a div

    Without a modal dialog, focus and the screen reader's reading cursor wander into the page behind the drawer, which is covered and makes no sense.

  • Three lines and no name

    An icon on its own is a guess for many people and silent to screen readers. The button says Menu, in text.

  • Focus left behind

    If focus stays on the Menu button after the drawer opens, the next Tab goes to the page underneath. Here it moves into the drawer.

  • No way back to where you were

    Closing the drawer without returning focus sends keyboard users back to the top of the page.

  • A new panel for every level

    Sliding in another panel for each section loses the context and needs its own back button. Sections here open in place.

Notes

Building it

  • Open it with showModal(): the page behind becomes inert and the drawer sits in the top layer, above any sticky header.
  • A click on the dimmed page closes the drawer too; Escape and the close button still work, so nobody depends on a pointer.
  • When the drawer opens, the section that holds the current page opens with it, so people see where they are.
  • After a link is chosen, focus goes to the new page's heading rather than back to the Menu button, because the page has changed.
  • The slide-in is short and switched off under reduced motion; the drawer then simply appears.

Sources: WAI-ARIA Authoring Practices: Dialog (Modal) · WAI-ARIA Authoring Practices: Disclosure

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction