Accessibility pattern · Navigation

Sidebar navigation

Groups open under real buttons with aria-expanded, and the open page is marked with aria-current="page" and more than color. Collapsed to a rail, the words are visually hidden rather than removed, so every icon is still a named link, and the same words appear as a tooltip on hover or focus.

WCAG criteria
6
Keyboard rules
5
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.

Kiran Studio

Invoices

Pick a page in the sidebar, then collapse it to the icon rail and hover or Tab through the icons.

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
Tab or ShiftTabMoves through the links and group buttons; links in a closed group are skipped.
Enter or SpaceOn a group button, opens or closes its links. In the rail, it widens the sidebar and opens the group.
EnterOn a link, opens that page (in the demo, marks it as the current page).
Enter or SpaceOn Collapse sidebar, switches between the full sidebar and the icon rail; focus stays on the button.
EscapeIn the rail, hides the tooltip that is showing, without moving focus.

Screen readers

What it announces

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

WhenExpected announcement
Tab reaches the current pageWorkspace, navigation. Invoices, current page, link
Tab reaches a closed groupProjects, button, collapsed
Collapse sidebar is pressedSidebar collapsed
Tab reaches a link in the railCustomers, link
Expand sidebar is pressedSidebar expanded

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-sidebar" data-ap-sidebar>
  <div class="ap-sidebar__frame">
    <div class="ap-sidebar__panel">
      <div class="ap-sidebar__brand">
        <span class="ap-sidebar__logo" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="m4 18 5.5-10 3.5 6 2-3.5L20 18Z"/></svg></span>
        <span class="ap-sidebar__text" translate="no">Kiran Studio</span>
      </div>
      <nav class="ap-sidebar__nav" aria-label="Workspace">
        <ul class="ap-sidebar__list">
          <li><a class="ap-sidebar__link" href="/home/"><svg class="ap-sidebar__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 10.5 12 4l8 6.5V19a1 1 0 0 1-1 1h-4.5v-5.5h-5V20H5a1 1 0 0 1-1-1Z"/></svg><span class="ap-sidebar__text">Home</span></a></li>
          <li><a class="ap-sidebar__link" href="/inbox/"><svg class="ap-sidebar__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 13H8l1.5 2.5h5L16 13h4.5"/><path d="M6 5h12l2.5 8v5a1 1 0 0 1-1 1h-15a1 1 0 0 1-1-1v-5Z"/></svg><span class="ap-sidebar__text">Inbox</span></a></li>
          <li class="ap-sidebar__group">
            <button type="button" class="ap-sidebar__toggle" aria-expanded="true" aria-controls="sb-billing"><svg class="ap-sidebar__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 3.5h12v17l-3-2-3 2-3-2-3 2Z"/><path d="M9.5 8.5h5M9.5 12.5h5"/></svg><span class="ap-sidebar__text">Billing</span><svg class="ap-sidebar__chevron" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg></button>
            <ul class="ap-sidebar__sub" id="sb-billing">
              <li><a class="ap-sidebar__sublink" href="/billing/invoices/" aria-current="page">Invoices</a></li>
              <li><a class="ap-sidebar__sublink" href="/billing/payments/">Payments</a></li>
              <li><a class="ap-sidebar__sublink" href="/billing/tax/">Tax reports</a></li>
            </ul>
          </li>
          <li class="ap-sidebar__group">
            <button type="button" class="ap-sidebar__toggle" aria-expanded="false" aria-controls="sb-projects"><svg class="ap-sidebar__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 7.5a2 2 0 0 1 2-2h3.8l2 2h7.2a2 2 0 0 1 2 2V17a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2Z"/></svg><span class="ap-sidebar__text">Projects</span><svg class="ap-sidebar__chevron" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg></button>
            <ul class="ap-sidebar__sub" id="sb-projects" hidden>
              <li><a class="ap-sidebar__sublink" href="/projects/website/">Website redesign</a></li>
              <li><a class="ap-sidebar__sublink" href="/projects/app/">Mobile app</a></li>
              <li><a class="ap-sidebar__sublink" href="/projects/brand/">Brand refresh</a></li>
            </ul>
          </li>
          <li><a class="ap-sidebar__link" href="/customers/"><svg class="ap-sidebar__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="9.5" cy="8.5" r="3.5"/><path d="M3 19.5a6.5 6.5 0 0 1 13 0"/><path d="M16 5.2a3.5 3.5 0 0 1 0 6.6M18.5 14.4a6.5 6.5 0 0 1 2.5 5.1"/></svg><span class="ap-sidebar__text">Customers</span></a></li>
          <li><a class="ap-sidebar__link" href="/settings/"><svg class="ap-sidebar__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7h8.5M16.5 7H20M4 17h2.5M10.5 17H20"/><circle cx="14.5" cy="7" r="2"/><circle cx="8.5" cy="17" r="2"/></svg><span class="ap-sidebar__text">Settings</span></a></li>
        </ul>
      </nav>
      <button type="button" class="ap-sidebar__collapse">
        <svg class="ap-sidebar__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3.5" y="4.5" width="17" height="15" rx="2"/><path d="M9 4.5v15"/><path class="ap-sidebar__flip" d="m15.5 10-2 2 2 2"/></svg>
        <span class="ap-sidebar__text">Collapse sidebar</span>
      </button>
    </div>
    <div class="ap-sidebar__main">
      <p class="ap-sidebar__crumb" translate="no">Kiran Studio</p>
      <h3 class="ap-sidebar__page">Invoices</h3>
      <p class="ap-sidebar__hint">Pick a page in the sidebar, then collapse it to the icon rail and hover or Tab through the icons.</p>
      <div class="ap-sidebar__mock" aria-hidden="true"><span></span><span></span><span></span></div>
    </div>
  </div>
  <p class="ap-sidebar__status" role="status"></p>
</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 sidebar is a nav landmark of nested lists, and each group's links follow the button that controls them.

  • 1.4.1 Use of Color Level A

    The current page has a tinted fill, a bar and bold text; its group is bold too, and keeps a bar in the rail.

  • 1.4.13 Content on Hover or Focus Level AA

    Rail tooltips appear on hover and keyboard focus, stay while the pointer moves onto them, and Escape hides them.

  • 2.5.3 Label in Name Level A

    In the rail each icon's name is the same word it showed before, and the tooltip shows that word, so voice commands still match.

  • 4.1.2 Name, Role, Value Level A

    Group buttons report expanded or collapsed, and the open page's link reports current page.

  • 4.1.3 Status Messages Level AA

    Collapsing or expanding the sidebar is confirmed in a status message, without moving focus.

Usage

When to use it

Use it

  • Apps and dashboards with many sections that people switch between all day.
  • Navigation that has to stay in view beside the work, with room to fold away.

Use something else

  • Content sites with a handful of pages: a header navigation is enough.
  • Phones, where the sidebar should become a drawer.
  • Commands such as Save or Export: they belong in a toolbar or a menu button.

Common failures

How it usually goes wrong

  • Icons with no name in the collapsed rail

    Hiding the words with display: none leaves links named nothing at all. Here they are visually hidden, so each icon is still a named link.

  • A title attribute as the label

    title tooltips do not appear on keyboard focus or touch, and screen readers handle them unevenly. The visible tooltip here is the link's own text.

  • Menu roles on site navigation

    role=menu tells screen readers to expect an application menu with arrow keys. Links and disclosure buttons are what navigation needs.

  • A group name that is both link and toggle

    If Billing opens a page and also expands, people cannot tell which will happen. Group names are buttons; pages are links inside them.

  • An expanded state that is not true

    If the rail hides a group's links but its button still says expanded, people look for links that are not there. The rail sets every group to collapsed.

  • Tooltips that cannot be dismissed

    A tooltip that covers the content beside it, with no way to close it, fails WCAG 1.4.13. Escape hides it here.

Notes

Building it

  • Groups follow the disclosure navigation pattern from the APG: a button with aria-expanded and aria-controls, the links in a list right after it.
  • The words in the rail use a visually hidden style, not display: none, and the same element becomes the tooltip, so what is shown and what is read never drift apart.
  • The collapse button's label switches between Collapse sidebar and Expand sidebar, so it has no aria-pressed as well; the status message confirms the change.
  • Remember the choice, for example in localStorage, so the sidebar stays the way people left it.
  • In a single-page app, opening a page should also move focus to its main heading; see the focus after a page change pattern.

Sources: WAI-ARIA Authoring Practices: Disclosure navigation menu example · Understanding WCAG 2.2: Content on hover or focus

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