Accessibility pattern · Overlays

Drawer

A drawer is a modal dialog that enters from the side: the same rules for focus, Escape and the page behind, in a different shape. Its header and footer stay put while the middle scrolls, so the close button and the main action are always in reach.

WCAG criteria
7
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.

Indigo Loom Handmade in Jaipur

Your cart

  • Block-print cotton kurta

    Indigo · Size M

    ₹1,890

  • Chanderi silk dupatta

    Rust · 2.25 m

    ₹2,450

  • Brass diya, set of two

    Hand-polished

    ₹780

  • Handmade paper notebook

    A5 · Dotted

    ₹350

Free returns within 30 days.

Subtotal ₹5,470

Delivery and taxes are worked out at checkout.

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 the Cart button, opens the drawer and moves focus to its close button.
Tab or ShiftTabMoves through the drawer's controls only; the page behind cannot be reached.
Arrow Up or Arrow DownIn a quantity field, changes it by one; the subtotal follows.
EscapeCloses the drawer and returns focus to the Cart button.
Enter or SpaceOn Remove, takes the item out and moves focus to the next item's Remove button, or to the drawer's heading when the cart is empty.

Screen readers

What it announces

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

WhenExpected announcement
The drawer opensYour cart, dialog. Close cart, button
Focus reaches the Cart buttonCart 4 items, button, opens a dialog
A quantity changesSubtotal ₹7,360
An item is removedItem removed. Subtotal ₹3,580
Checkout is pressedCheckout started (focus is back on the Cart 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-drawer" data-ap-drawer>
  <div class="ap-drawer__bar">
    <p class="ap-drawer__brand">
      <svg class="ap-drawer__mark" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.5 21.5 12 12 21.5 2.5 12Z"/><path d="M12 8l4 4-4 4-4-4Z"/></svg>
      <span class="ap-drawer__store"><span class="ap-drawer__store-name">Indigo Loom</span> <span class="ap-drawer__tag">Handmade in Jaipur</span></span>
    </p>
    <button type="button" class="ap-btn ap-drawer__open" data-ap-open aria-haspopup="dialog">
      <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5.5 7.5h13l1 13h-15Z"/><path d="M9 10V6.5a3 3 0 0 1 6 0V10"/></svg>
      <span>Cart</span>
      <span class="ap-drawer__badge" data-ap-badge><span data-ap-count>4</span> <span class="ap-drawer__vh" data-ap-unit>items</span></span>
    </button>
  </div>
  <p class="ap-drawer__status" role="status" data-ap-status></p>

  <dialog class="ap-drawer__panel" aria-labelledby="drawer-name">
    <form class="ap-drawer__form" method="dialog">
      <div class="ap-drawer__top">
        <h2 class="ap-drawer__name" id="drawer-name" tabindex="-1">Your cart</h2>
        <button type="button" class="ap-drawer__close" data-ap-close aria-label="Close cart" autofocus>
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18"/></svg>
        </button>
      </div>

      <div class="ap-drawer__body">
        <ul class="ap-drawer__items" data-ap-items>
          <li class="ap-drawer__item" data-price="1890">
            <span class="ap-drawer__thumb ap-drawer__thumb--1" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M9 3.5 4 6.5l2 4 2-1V21h8V9.5l2 1 2-4-5-3a3 3 0 0 1-6 0Z"/></svg></span>
            <div class="ap-drawer__info">
              <p class="ap-drawer__item-name">Block-print cotton kurta</p>
              <p class="ap-drawer__meta">Indigo · Size M</p>
            </div>
            <p class="ap-drawer__price">₹<span data-ap-line>1,890</span></p>
            <div class="ap-drawer__row">
              <label class="ap-drawer__qty-label" for="drawer-qty-1">Quantity</label>
              <input class="ap-drawer__qty" id="drawer-qty-1" type="number" inputmode="numeric" min="1" max="10" value="1" />
              <button type="button" class="ap-drawer__remove" data-ap-remove aria-label="Remove Block-print cotton kurta">Remove</button>
            </div>
          </li>
          <li class="ap-drawer__item" data-price="2450">
            <span class="ap-drawer__thumb ap-drawer__thumb--2" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M4 6.5c2.7-2 5.3 2 8 0s5.3-2 8 0v11c-2.7-2-5.3 2-8 0s-5.3-2-8 0Z"/><path d="M8 9.5v6M16 9.5v6"/></svg></span>
            <div class="ap-drawer__info">
              <p class="ap-drawer__item-name">Chanderi silk dupatta</p>
              <p class="ap-drawer__meta">Rust · 2.25 m</p>
            </div>
            <p class="ap-drawer__price">₹<span data-ap-line>2,450</span></p>
            <div class="ap-drawer__row">
              <label class="ap-drawer__qty-label" for="drawer-qty-2">Quantity</label>
              <input class="ap-drawer__qty" id="drawer-qty-2" type="number" inputmode="numeric" min="1" max="10" value="1" />
              <button type="button" class="ap-drawer__remove" data-ap-remove aria-label="Remove Chanderi silk dupatta">Remove</button>
            </div>
          </li>
          <li class="ap-drawer__item" data-price="780">
            <span class="ap-drawer__thumb ap-drawer__thumb--3" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M12 3c2 2.6 2 4.8 0 7-2-2.2-2-4.4 0-7Z"/><path d="M3.5 13h17c0 4-3.8 7-8.5 7s-8.5-3-8.5-7Z"/></svg></span>
            <div class="ap-drawer__info">
              <p class="ap-drawer__item-name">Brass diya, set of two</p>
              <p class="ap-drawer__meta">Hand-polished</p>
            </div>
            <p class="ap-drawer__price">₹<span data-ap-line>780</span></p>
            <div class="ap-drawer__row">
              <label class="ap-drawer__qty-label" for="drawer-qty-3">Quantity</label>
              <input class="ap-drawer__qty" id="drawer-qty-3" type="number" inputmode="numeric" min="1" max="10" value="1" />
              <button type="button" class="ap-drawer__remove" data-ap-remove aria-label="Remove Brass diya, set of two">Remove</button>
            </div>
          </li>
          <li class="ap-drawer__item" data-price="350">
            <span class="ap-drawer__thumb ap-drawer__thumb--4" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><rect x="5" y="3" width="14" height="18" rx="2"/><path d="M9 3v18M12.5 8h3M12.5 11.5h3"/></svg></span>
            <div class="ap-drawer__info">
              <p class="ap-drawer__item-name">Handmade paper notebook</p>
              <p class="ap-drawer__meta">A5 · Dotted</p>
            </div>
            <p class="ap-drawer__price">₹<span data-ap-line>350</span></p>
            <div class="ap-drawer__row">
              <label class="ap-drawer__qty-label" for="drawer-qty-4">Quantity</label>
              <input class="ap-drawer__qty" id="drawer-qty-4" type="number" inputmode="numeric" min="1" max="10" value="1" />
              <button type="button" class="ap-drawer__remove" data-ap-remove aria-label="Remove Handmade paper notebook">Remove</button>
            </div>
          </li>
        </ul>
        <div class="ap-drawer__empty" data-ap-cart-empty hidden>
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5.5 7.5h13l1 13h-15Z"/><path d="M9 10V6.5a3 3 0 0 1 6 0V10"/></svg>
          <p>Your cart is empty.</p>
        </div>
        <p class="ap-drawer__perk">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 12a8 8 0 1 0 2.4-5.7"/><path d="M4 4v4.5h4.5"/></svg>
          Free returns within 30 days.
        </p>
        <p class="ap-drawer__vh" role="status" data-ap-live></p>
      </div>

      <div class="ap-drawer__foot">
        <p class="ap-drawer__sum" aria-live="polite" aria-atomic="true"><span>Subtotal</span> <span class="ap-drawer__amount">₹<span data-ap-subtotal>5,470</span></span></p>
        <p class="ap-drawer__fine">Delivery and taxes are worked out at checkout.</p>
        <div class="ap-drawer__actions">
          <button type="button" class="ap-btn" data-ap-close>Keep shopping</button>
          <button type="submit" class="ap-btn ap-btn--primary" value="checkout" data-ap-checkout>
            Checkout
            <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
          </button>
        </div>
      </div>
    </form>
  </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 is named by its heading, and every quantity has a real label, so the structure is exposed, not just drawn.

  • 2.1.2 No Keyboard Trap Level A

    Focus stays in the drawer while it is open, and Escape, the close button and Keep shopping always let it out.

  • 2.4.3 Focus Order Level A

    Focus moves into the drawer on open, back to the Cart button on close, and to a sensible neighbour when an item is removed.

  • 2.4.11 Focus Not Obscured (Minimum) Level AA

    The header and footer are fixed rows and only the middle scrolls, so nothing sticky covers the control that has focus.

  • 1.4.10 Reflow Level AA

    The drawer is never wider than the screen; on a phone it takes the full width instead of squeezing the content.

  • 4.1.2 Name, Role, Value Level A

    The dialog element gives the role and modal state, and the Cart button's name includes how many items are in it.

  • 4.1.3 Status Messages Level AA

    Removing an item and changing the subtotal are announced from live regions inside the drawer, without moving focus.

Usage

When to use it

Use it

  • A secondary task that belongs to the current page, like a cart, filters or the details of a row.
  • Content tall enough to need its own scroll, with actions that must stay in view.

Use something else

  • The main navigation of a site on a phone: use the mobile navigation pattern, which is built for it.
  • A short question or confirmation: a centred modal or alert dialog is quicker to read.
  • Something people need to keep open while they work on the page: use a non-modal panel.

Common failures

How it usually goes wrong

  • A sliding div that is not a dialog

    A panel moved on screen with a transform leaves the page behind fully reachable, so Tab and a screen reader wander out from under it.

  • Hidden but still focusable

    A closed drawer parked off screen with its controls still in the Tab order makes keyboard users tab through things they cannot see. A closed dialog is not rendered at all.

  • The footer covers the focused control

    A position: fixed footer laid over the list hides whatever is focused under it. Here the footer is its own row and only the body scrolls.

  • Focus lost after removing an item

    Deleting the element that had focus drops focus to the page. The script moves it to the next item's Remove button, or to the heading.

  • The page scrolls underneath

    Scrolling the page behind a drawer is disorienting, and on touch screens it often scrolls the wrong thing. The page is locked while the drawer is open.

  • Motion with no way to turn it off

    A long slide can trigger nausea. This one is short, and it switches off when the system asks for reduced motion.

Notes

Building it

  • Open it with showModal(): the top layer, the inert page and Escape come from the browser, the same as a centred dialog.
  • Position it with logical properties (inset-inline-end), so in a right-to-left language it slides in from the left without extra code.
  • Clicking the shaded area closes the drawer, because nothing in a cart is lost by closing it. For a drawer holding an unsaved form, leave that out.
  • Lock the page by setting overflow: hidden on the html element while it is open, and keep the scrollbar's space (scrollbar-gutter) so the page does not jump.

Sources: WAI-ARIA Authoring Practices: Dialog (Modal) · HTML: the dialog element

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