Headless commerce and accessibility: the hidden compliance gap
Traditional ecommerce platforms like Shopify, WooCommerce and Magento ship with opinionated themes. Those themes have accessibility problems, but they at least start with semantic HTML. When a brand switches to a headless architecture, they take ownership of every element. Custom React components replace platform templates. The semantic structure that came for free is now the team's responsibility.
The result: headless stores consistently score worse on accessibility audits than their platform-default counterparts, despite using more modern technology. The engineering sophistication that headless requires does not automatically extend to accessibility knowledge.
Why headless creates accessibility risk
In a headless architecture, the frontend is decoupled from the commerce backend. The frontend team builds the entire UI from scratch, typically in React, Next.js, Nuxt or Remix. Every interaction element is a custom component: not a native HTML button but a div with a click handler. Getting ARIA patterns right becomes essential. Not a native select but a custom dropdown. Not a native form element but a styled input component.
Native HTML elements come with built-in accessibility. A button element is keyboard focusable, activatable with Space and Enter, announces "button" to screen readers and exposes its label. A div with a click handler does none of these things unless the developer explicitly adds tabindex="0", role="button", keyboard event handlers and an accessible name. Every custom component requires this work. Few teams do it consistently.
The volume of custom components in a headless store is high. Product image galleries, color swatch selectors, size pickers, quantity inputs, wishlist toggles, cart drawers, notification toasts, modal dialogs, accordion FAQs: all of these are custom in a headless build, and all of them have specific ARIA and keyboard interaction requirements.
React and Next.js SPA accessibility pitfalls
Single-page applications break the default browser behavior that screen reader users rely on. In a traditional multi-page website, when a user clicks a link, the browser loads a new page and the screen reader announces the new page title and resets focus to the top of the document. In a React SPA, this does not happen. The URL changes, the content changes, but no page load occurs. Screen readers have no way to detect the change.
This is the client-side routing problem. WCAG 2.4.2 requires that pages have titles that describe their topic. WCAG 2.4.3 requires that focus follows a logical order. Both are part of the WCAG 2.1 AA ecommerce checklist. When a SPA navigates without triggering a page load, neither condition is automatically met.
Solutions for React/Next.js client-side routing:
- Announce route changes via a live region: a visually hidden
divwitharia-live="polite"andaria-atomic="true"that is updated with the new page title on each route change. Screen readers announce the content of live regions when they change. - Move focus on navigation: after a route change, move focus to the main heading (
h1) or the main landmark on the new page. This replicates the browser's natural behavior for full page loads. - Update the document title: Next.js provides the
Headcomponent and themetadataexport for this. Every page should have a unique, descriptive title that changes on navigation.
Next.js 13+ App Router has improved this: the built-in router includes a route announcement mechanism. But it requires the page's h1 or document title to be populated, and custom navigation implementations that bypass the router do not benefit from it.
Loading states and async content
Headless stores rely heavily on client-side data fetching. A product page might load the base HTML server-side but fetch inventory, pricing and recommendations client-side. While data loads, the UI shows skeleton screens or spinners.
Loading states have specific accessibility requirements. A screen reader user who arrives on a page where the price is loading sees nothing where the price should be. If there is no announcement when loading completes, they never know the price appeared.
Use aria-live="polite" regions to announce loading completion. For skeleton screens, use aria-busy="true" on the container while loading and aria-busy="false" when content appears. Spinners should have accessible names via aria-label and should use role="status" so screen readers announce when loading is complete.
Suspense boundaries in React 18 and streaming SSR in Next.js App Router enable progressive rendering. This is positive for accessibility: content appears in a predictable order rather than in a random async order. But Suspense fallbacks (the loading states) still need accessible labels.
Headless checkout accessibility
Checkout is where headless accessibility failures cost the most. A user who cannot complete checkout cannot purchase. The conversion impact is direct.
Headless checkout implementations typically use one of three approaches:
- Custom headless checkout: the team builds every step of checkout as custom components. Highest flexibility, highest accessibility risk. Every form field, every step indicator, every error state is custom.
- Embedded checkout iframes: the payment step is an iframe from the payment processor (Stripe Elements, Braintree hosted fields). The surrounding flow is custom. The payment step inherits the processor's accessibility.
- Redirect to hosted checkout: the user is redirected to the platform's hosted checkout (Shopify's checkout, BigCommerce's checkout). These have been accessibility-audited by the platform. The trade-off is less customization.
For fully custom headless checkout, the step indicator (Step 1: Contact. Step 2: Shipping. Step 3: Payment) is a common failure. It is usually built as a visual list with active states. Without proper aria-current="step" on the active step, screen reader users cannot determine where they are in the process. Correct ARIA labeling is critical for these custom components.
Multi-step checkout state management creates focus problems. When the user advances from step 1 to step 2, focus must move to the heading or first input of step 2. If it stays on the "Continue" button (which may have been replaced by a "Back" button in the new step), the user is disoriented.
Composable commerce and responsibility gaps
Composable commerce takes headless further by composing the commerce stack from best-of-breed services: Contentful for CMS, Algolia for search, Klaviyo for email capture forms, Yotpo for reviews, Gorgias for chat. Each service contributes UI components or widgets to the frontend.
The responsibility gap: each vendor maintains their component. When their component has an accessibility failure, who is responsible? Legally: you are. The site operator is responsible for the accessibility of their site, regardless of which vendor produced each component. The ADA and EAA do not recognize "my vendor's widget did it" as a defense.
In practice, this means vendor-supplied components must be evaluated before deployment. Ask vendors directly: has this component been audited for WCAG 2.1 AA? Can you provide an ACR? What is your process for reporting and fixing accessibility issues?
Common composable commerce accessibility failures:
- Algolia InstantSearch: the refinement list (faceted navigation) has keyboard accessibility issues in some configurations. Price range sliders lack input alternatives by default.
- Yotpo reviews widgets: star rating inputs are often not keyboard-accessible. Review forms have label association issues.
- Chat widgets (Gorgias, Intercom, Zendesk): these frequently fail focus management. Opening the chat drawer may not move focus into it. Closing it may lose the user's position.
- Email capture popups (Klaviyo, Attentive): modal dialogs that do not trap focus, do not have close buttons accessible via keyboard, or do not return focus to the trigger when closed.
Shopify Hydrogen and Remix
Shopify Hydrogen is the official headless framework for Shopify, built on Remix. It provides a set of components for common ecommerce patterns: cart, product form, image gallery.
Hydrogen's component library has improved accessibility support in recent versions. The CartForm component uses native HTML form elements. The Image component enforces the alt attribute. But Hydrogen's storefront components are starting points, not fully accessible out of the box.
Common Hydrogen accessibility gaps: the cart drawer (added in Hydrogen 2.x) lacks focus trapping by default. Custom variant selectors built on top of Hydrogen's product form need ARIA state management. The pagination component does not announce results count to screen readers.
Remix's data loading model (loaders and actions) is fundamentally good for accessibility because it degrades gracefully to full page refreshes when JavaScript is unavailable. This means forms submit, navigation works and state changes are visible to screen readers without client-side JavaScript. This is a significant architectural accessibility advantage over pure CSR React apps.
Medusa, Commerce Layer and other headless backends
Open-source headless commerce backends like Medusa and Commerce Layer provide APIs and some starter frontend templates. These templates have varying levels of accessibility investment.
Medusa's Next.js starter (Medusa Next.js Starter) has known accessibility issues: keyboard navigation gaps in the product gallery, missing focus indicators on several interactive components, and incomplete ARIA labeling in the checkout flow. These are known to the community but not prioritized by default.
If you are building on an open-source headless starter, treat the starter as a starting point for design, not a compliance baseline. Audit it before launch. Do not assume the community has handled accessibility.
PWA accessibility considerations
Progressive Web Apps are increasingly common in headless commerce. PWAs add offline support, install prompts and native-app-like behavior. They also add accessibility challenges specific to the PWA model.
The install prompt (the browser's "Add to home screen" prompt) is controlled by the browser, not the developer. Triggering it via an in-app button requires that the button be accessible. The install prompt dialog itself is accessible in all major browsers.
Offline states must be communicated accessibly. If a user is offline and tries to navigate, the offline fallback page must be accessible. It must also communicate the offline state clearly: "You are offline. Some features are unavailable." A blank white page or a browser error is not accessible.
Push notification permission dialogs are browser-controlled and accessible. Custom pre-permission prompts that try to prime users before the browser dialog are often inaccessible: they are frequently built as styled div overlays without focus management or ARIA roles.
Testing headless sites for accessibility
Headless sites present specific testing challenges. Server-side rendered HTML may pass automated scans while client-side rendered content fails. Async content that loads after the initial DOM parse is missed by static analysis tools. See our accessibility testing tools comparison for which tools handle dynamic content best.
Effective testing for headless requires:
- Dynamic testing with a real browser: axe-core running in Playwright or Cypress, after the page has fully loaded and client-side content has rendered. Static HTML analysis misses headless-specific failures.
- Route change testing: trigger navigation events and verify that focus management and live region announcements fire correctly.
- Loading state testing: test pages during data fetching, not just after. Verify that loading indicators have accessible names and that completion is announced.
- Modal and drawer testing: open and close every modal, drawer and overlay. Verify focus traps, close mechanisms and focus return.
- Screen reader testing: VoiceOver on Safari (macOS and iOS), NVDA on Chrome (Windows) and TalkBack on Android. Automated tools catch 30-40% of issues. Manual screen reader testing catches the rest.
Frequently asked questions
- Is headless commerce less accessible than traditional platforms?
- Headless commerce is not inherently less accessible, but it shifts accessibility responsibility entirely to the development team. Traditional platforms like Shopify provide built-in accessible checkout flows. Headless implementations require building all accessibility features from scratch.
- What are the biggest accessibility risks in headless ecommerce?
- Client-side rendering without proper ARIA live regions, custom checkout flows missing keyboard navigation, focus management failures in SPA route transitions and missing semantic HTML in component libraries are the most common issues.
- Does headless commerce change PCI DSS scope?
- Often, yes. Moving from a hosted checkout (SAQ A) to a headless checkout with custom payment form elements can expand PCI scope to SAQ A-EP or higher. Verify your SAQ classification before completing a headless migration.
Complyko tests your headless checkout and cart flows using a real browser with JavaScript enabled. For platform-specific guidance, see our guides on Shopify accessibility and WooCommerce accessibility. See what your store actually looks like to a screen reader user.
Get your free scan →