Skip to content
Accessibility Helper

Shopify cart drawer and menu drawer: keyboard and screen reader fixes

What a Shopify cart drawer and menu drawer must do for keyboard and screen reader users, how Dawn does it, and how to fix drawers that apps broke.

A cart drawer or menu drawer works for everyone when keyboard focus moves into it as it opens, stays inside while it’s open, leaves when you press Esc, and returns to the button that opened it, and when a screen reader hears “Cart, dialog” instead of silence. Shopify’s Dawn theme does all of this out of the box. Most broken drawers come from cart apps, menu apps and custom code, so this guide shows you how to test yours and how to fix what you find.

What must a drawer do for keyboard and screen reader users?

A drawer is a dialog that slides in from the side. The W3C’s modal dialog pattern sets out what it owes people who don’t use a mouse, and the WCAG 2.2 criteria behind each point are below.

  • The opener says what it does. The cart icon and the menu icon need names, such as “Cart” and “Menu”, and ideally say that they open a dialog or that the menu is expanded or collapsed (4.1.2 Name, Role, Value, Level A).
  • It opens from the keyboard. Enter or Space on the opener opens it. Nothing opens on hover alone (2.1.1 Keyboard, Level A).
  • Focus moves in. When it opens, focus lands on the drawer itself, its heading or its first control, not on the page behind (2.4.3 Focus Order, Level A).
  • Focus stays in. Tab and Shift+Tab cycle through the drawer’s controls and don’t wander into the page underneath.
  • It closes from the keyboard. Esc closes it, and so does a close button with a real name (2.1.2 No Keyboard Trap, Level A).
  • Focus returns. After it closes, focus is back on the button that opened it.
  • It has a name and a role. The drawer is marked as a dialog and named, so a screen reader announces “Cart, dialog” (1.3.1 Info and Relationships, Level A).
  • You can see where you are. Every control inside shows a visible focus outline (2.4.7 Focus Visible, Level AA), and nothing covers it (2.4.11 Focus Not Obscured (Minimum), Level AA).

Shopify’s own theme best practices say the same: focus moves into drawers and modals, stays inside while open, and returns to the launcher on close.

How does Dawn’s cart drawer do it?

Dawn’s source is on GitHub; this is what Dawn 16 ships with. The drawer’s inner container is a named modal dialog that can take focus:

<cart-drawer class="drawer">
  <div id="CartDrawer" class="cart-drawer">
    <div id="CartDrawer-Overlay" class="cart-drawer__overlay"></div>
    <div
      class="drawer__inner"
      role="dialog"
      aria-modal="true"
      aria-label="{{ 'sections.cart.title' | t }}"
      tabindex="-1"
    >
      ...
      <button class="drawer__close" type="button" aria-label="{{ 'accessibility.close' | t }}">
        {{- 'icon-close.svg' | inline_asset_content -}}
      </button>

In English, sections.cart.title is “Cart” and accessibility.close is “Close”, so a screen reader announces “Cart, dialog” when the drawer opens and “Close, button” on the close button. The tabindex="-1" lets the theme’s script move focus to the drawer without adding it to the Tab order.

The script, in assets/cart-drawer.js, does the rest. When the drawer opens it remembers which element opened it, calls trapFocus() on the drawer so Tab cycles inside, and listens for Esc to close. When it closes it calls removeTrapFocus() with that remembered element, which puts focus back on the cart icon. It also adds aria-haspopup="dialog" to the cart link so the opener tells people a dialog is coming.

The cart icon itself, in the header, is an icon plus visually hidden text:

<a href="{{ routes.cart_url }}" class="header__icon header__icon--cart link focus-inset" id="cart-icon-bubble">
  <span class="svg-wrapper">{{ 'icon-cart.svg' | inline_asset_content }}</span>
  <span class="visually-hidden">{{ 'templates.cart.cart' | t }}</span>
  <div class="cart-count-bubble">...</div>
</a>

Inside the drawer, each line’s quantity controls are named too: the buttons read “Increase quantity for” and “Decrease quantity for” followed by the product’s title, and the number field is labeled “Quantity for” the product. Without the product name, a screen reader user in a cart with three items would hear “Increase quantity, button” three times with no way to tell them apart.

How does Dawn’s menu drawer do it?

The menu drawer takes a different route. It’s built on the HTML details and summary elements, which give you an open-and-close control without any script:

<header-drawer>
  <details id="Details-menu-drawer-container" class="menu-drawer-container">
    <summary
      class="header__icon header__icon--menu header__icon--summary link focus-inset"
      aria-label="{{ 'sections.header.menu' | t }}"
    >
      <span>
        {{- 'icon-hamburger.svg' | inline_asset_content -}}
        {{- 'icon-close.svg' | inline_asset_content -}}
      </span>
    </summary>
    <div id="menu-drawer" class="menu-drawer">
      <nav class="menu-drawer__navigation">
        ...

The summary is the hamburger button, named “Menu” through sections.header.menu. Dawn’s global.js sets aria-expanded on it as the menu opens and closes, traps focus inside the open drawer, and closes it on Esc with focus back on the button. Each submenu is its own nested details with a back button, so a keyboard user can go down a level and come back.

How do you test a drawer in five minutes?

Do this on your live store, in a normal browser window, without touching the mouse.

  1. Reach the cart icon. Press Tab from the top of the page until the cart icon shows a focus outline. If you can’t see where focus is, that’s your first finding.
  2. Open it. Press Enter. Focus should move into the drawer: the outline appears on the drawer, its heading or its close button.
  3. Stay inside. Press Tab a dozen times. Focus should cycle through the drawer’s quantity buttons, remove links, note field and checkout button, and come back around. If the outline appears on the page behind the drawer, focus isn’t trapped.
  4. Close it. Press Esc. The drawer should close and focus should return to the cart icon. Open it again, Tab to the close button, press Enter, and check the same thing.
  5. Add to cart. Go to a product page, Tab to “Add to cart” and press Enter. The drawer should open and focus should move into it, exactly as in step 2.
  6. Open the menu. Tab to the menu button, press Enter, move through the links, open a submenu, come back, and press Esc. Focus should end up on the menu button.

Then a screen reader spot check. On a Mac, press Command+F5 to start VoiceOver and use Tab the same way. On Windows, NVDA is a free screen reader. Listen for:

  • “Cart” and “Menu” on the icons, followed by their role. Hearing “button” or “link” alone means the name is missing.
  • “Cart, dialog” or similar when the drawer opens.
  • “Close, button” on the close button.
  • Product names on the quantity buttons and the remove links.

For Shopify stores

Find and fix accessibility issues on your store

CodeInspire Accessibility Tool adds an accessibility widget for your shoppers, audits your pages against WCAG, and gives you step-by-step fixes for your Shopify admin, theme editor or theme code.

Get it on the Shopify App StoreWhat the app does

What usually breaks a drawer, and how do you fix it?

A cart app replaced the drawer

Upsell, slide cart, sticky cart and “free shipping bar” apps often swap the theme’s drawer for their own. Some are careful; some aren’t. Run the five-minute test after installing one, ask the developer how they test with a keyboard and a screen reader, and if the drawer fails and the developer can’t fix it, remove the app. Keeping Dawn’s drawer and adding a smaller app for the extra feature is usually safer.

Icon buttons with no name

The close button, the quantity stepper and remove icons are the usual victims. The fix is a name that screen readers can read. Either visually hidden text or aria-label works:

<button type="button" class="drawer__close" aria-label="Close">
  <svg aria-hidden="true" focusable="false">...</svg>
</button>

<button type="button" name="plus">
  <span class="visually-hidden">Increase quantity for Linen shirt</span>
  <svg aria-hidden="true" focusable="false">...</svg>
</button>

Hide the icon from assistive technology with aria-hidden="true" so the name isn’t followed by the SVG’s own title or nothing at all.

Focus stays on the page behind

If a drawer opens but the focus outline stays on the cart icon, keyboard users have to Tab through the whole page to reach it, and screen reader users may never find it. The drawer needs tabindex="-1" and a script that focuses it on open and returns focus on close:

function openDrawer(drawer, opener) {
  drawer.hidden = false;
  drawer.dataset.openerId = opener.id;
  drawer.focus(); // the drawer has tabindex="-1"
}

function closeDrawer(drawer) {
  drawer.hidden = true;
  document.getElementById(drawer.dataset.openerId)?.focus();
}

Add a focus trap while it’s open, or use a native dialog element opened with showModal(), which traps focus and handles Esc for you. Our keyboard accessibility guide shows that pattern.

A mega menu that opens when the pointer passes over it and has no keyboard equivalent fails 2.1.1 outright. Each top-level item needs to be a button or a summary that opens on Enter or Space, with aria-expanded to say which state it’s in. Dawn’s details and summary approach is the simplest fix because the browser handles the opening and closing.

Focus outlines removed

A *:focus { outline: none; } rule in custom CSS hides focus everywhere, including inside drawers. Delete it. Dawn uses :focus-visible, so mouse users don’t see outlines anyway.

Nothing is announced when an item is added

When a shopper adds a product to the cart, Dawn opens the drawer and moves focus into it, and that movement is the announcement. If an app replaces this with a small toast or a count that changes silently, screen reader users don’t know anything happened. Put the confirmation in a live region, as Dawn does for its product gallery with a visually hidden role="status" element:

<div class="visually-hidden" role="status" aria-live="polite">
  Linen shirt added to your cart. 2 items in cart.
</div>

Update that element’s text when the cart changes, and a screen reader reads it without moving focus.

Small or crowded controls

Quantity buttons and remove icons in a drawer are often tiny. WCAG 2.2 asks for targets of at least 24 by 24 CSS pixels, or enough space around them (2.5.8 Target Size (Minimum), Level AA). Shopify’s theme best practices go further and recommend 44 by 44 pixels for primary controls such as the cart and close buttons.

What won’t a keyboard test tell you?

A keyboard test proves that focus moves correctly and that every control is reachable. It can’t tell you whether the names make sense to someone who can’t see the icon, or whether the order of announcements is confusing, and that’s what the screen reader spot check is for. Automated tools are weaker still here: they only see a drawer if it’s open when the page loads, so a closed drawer passes every automated check no matter what’s inside it. Our website checker says as much in its results: menus, pop-ups and cart drawers need checking by hand.

This guide describes Dawn 16’s code. Older versions, edited themes and other themes can differ. Read your own theme’s cart drawer and header snippets, and test the store itself rather than trusting the source.

What should you do next?

  1. Run the five-minute test above on your cart drawer and menu drawer, on desktop and on a phone with a keyboard or screen reader.
  2. Test every cart or menu app you’ve installed the same way, and keep a note of which ones pass.
  3. Run the free website accessibility checker on your home page and a product page to catch the problems that do show in the code, such as unnamed buttons and missing labels, then work through the “check these by hand” list it gives you.

Frequently asked questions

How do I test my Shopify cart drawer with a keyboard?

Press Tab until the cart icon has focus, then press Enter. Focus should move into the drawer, Tab and Shift+Tab should stay inside it, Esc should close it, and focus should land back on the cart icon. Then add a product to the cart from a product page and check that the drawer opens and focus moves into it the same way.

What should a screen reader announce when the cart drawer opens?

The drawer's name and role, such as Cart, dialog, followed by the first thing inside it. Each item's name, price and quantity controls should be announced clearly, and the close button should be announced as Close, not just button. If you hear only button or nothing at all, the drawer or its buttons are missing accessible names.

Does the Dawn theme's cart drawer work with a keyboard?

Yes. Dawn marks the drawer as a modal dialog named Cart, moves focus into it when it opens, keeps focus inside while it's open, closes it on Esc and returns focus to the cart icon. The menu drawer uses the details and summary elements with a button named Menu and the same focus handling. Apps and custom code can still break either one.

Why does my cart drawer break after installing a cart app?

Many upsell, slide cart and sticky cart apps replace the theme's drawer with their own, and not all of them manage focus, name their buttons or close on Esc. Test the drawer with a keyboard after installing any cart app, ask the developer how they test accessibility, and remove the app if it fails.

Sources