Shopify Focus Trap — Why Missing It Is an Automatic Theme Store Rejection
When a modal dialog or cart drawer opens in a Shopify theme and a keyboard user presses Tab, focus should cycle through only the interactive elements inside that dialog — never escape to the page behind. When focus escapes the dialog, keyboard users lose context, screen reader users hear content from the background page, and the interface is effectively unusable without a mouse. Shopify Theme Store reviewers check for focus trap specifically. Missing it is one of the most common automatic rejection reasons. This guide gives you a production-ready implementation.
What a focus trap is and why it is required
A focus trap confines keyboard focus to a specific DOM subtree — typically a modal dialog, cart drawer, or mobile navigation — while it is visible. Without it: a keyboard user opening a modal can Tab through the modal, exit to the page behind, and lose the modal entirely. A screen reader user hears background content while the modal is supposedly active. This violates WCAG 2.1 Success Criterion 2.1.2 (No Keyboard Trap) in reverse — the issue is focus escaping, not focus being trapped inappropriately. ARIA's authoring practices require focus trap for all role=dialog elements.
How to implement a focus trap utility
A correct focus trap: 1. Finds all focusable elements inside the dialog (buttons, links, inputs, selects, textareas, elements with tabindex >= 0). 2. On Tab keypress, if focus is on the last focusable element, moves it to the first. 3. On Shift+Tab, if focus is on the first focusable element, moves it to the last. 4. On Escape keypress, closes the dialog and returns focus to the element that opened it. 5. When the trap is removed, cleans up all event listeners. Dawn's focusTrap utility in global.js is the reference implementation — use it or adapt it.
Implementing focus trap with focusable element selection
The focusable elements query: const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'. To trap: function trapFocus(container) { const elements = [...container.querySelectorAll(FOCUSABLE)].filter(el => !el.closest('[hidden]')); const first = elements[0]; const last = elements[elements.length - 1]; container.addEventListener('keydown', function onKeydown(e) { if (e.key !== 'Tab') return; if (e.shiftKey ? document.activeElement === first : document.activeElement === last) { e.preventDefault(); (e.shiftKey ? last : first).focus(); } }); first.focus(); return onKeydown; }
Returning focus when the dialog closes
When closing a modal or drawer, return focus to the element that triggered it — typically the button that opened it. Store the trigger before opening: const opener = document.activeElement; trapFocus(dialog); // ... when closing: dialog.close(); opener.focus(). Without this, keyboard users who close a modal find their focus position lost — they must Tab from the beginning of the page. This is another automatic Theme Store rejection criterion and a WCAG 2.4.3 violation.
The fix: before and after
// CODE_COMPARISON
Frequently asked questions
- Is focus trap required for all interactive overlays in Shopify themes?
Focus trap is required for all elements that use role=dialog or role=alertdialog, cart drawers, mobile navigation drawers, search overlays, and any element that covers and visually obscures the background page. It is not required for dropdown menus (which use a different ARIA pattern with arrow key navigation) or tooltips. When in doubt: if a keyboard user should not be able to interact with background content while the element is open, it needs a focus trap.
- What happens when a modal has no focusable elements?
If a modal has no interactive elements (a pure notification dialog), add tabindex='-1' to the modal container itself and focus it: modal.setAttribute('tabindex', '-1'); modal.focus(). This moves keyboard focus into the dialog without any Tab cycling. The Escape key handler should still close the dialog and return focus to the opener. Without this, focus remains on whatever was focused before the modal opened, which confuses screen reader users.
- How do I handle nested focus traps (a modal inside a drawer)?
When a secondary dialog opens inside an already-trapped context, release the outer focus trap temporarily and apply the inner trap. When the inner dialog closes, release it and reapply the outer trap. Never stack traps without releasing the outer one — this creates a situation where Tab behavior is managed by two competing handlers, producing unpredictable focus movement.
// SCAN_YOUR_CODE
Does your theme have this bug?
Paste your code. Syphio automatically detects and fixes this error and hundreds of others — in seconds.
Check my JavaScript →