The most common Shopify accessibility audit findings, and how to fix them in theme code
The accessibility findings auditors report most on Shopify stores, where each one lives in an Online Store 2.0 theme, and the code that fixes it.
Short answer
Most accessibility findings on Shopify stores fall into two groups. The first is the six failures automated scans catch on almost every website: low-contrast text, missing alt text, missing form labels, empty links, empty buttons and a missing page language. The second only shows up with a keyboard and a screen reader: removed focus outlines, hover-only menus, carousels that won't stop, popups and cart drawers that don't manage focus, variant pickers built from <div>s, and "added to cart" messages nobody hears. Almost all of them live in a dozen theme files, so one fix usually clears every instance.
This guide goes through each finding the way an auditor reports it: the WCAG 2.2 success criterion, where it usually lives in an Online Store 2.0 theme (file names follow Dawn-derived themes), and the fix. The code comes from our demo store (fictional), a Shopify-style theme seeded with 30 mock findings that we then fixed and retested with scripts. The change log has every entry.
Why these findings, and why theme code
WebAIM's 2026 scan of the top one million home pages found detectable WCAG failures on 95.9% of them. The most common were low-contrast text (83.9% of pages), missing image alt text (53.1%), missing form labels (51%), empty links (46.3%), empty buttons (30.6%) and missing page language (13.5%). WebAIM found that 96% of all detected errors fall into those six categories (WebAIM Million 2026).
Shopify's Theme Store sets a baseline for the themes it lists: an average Lighthouse accessibility score of at least 90 across home, collection and product pages on desktop and mobile, keyboard-accessible dropdown navigation, visible focus, labelled inputs, 4.5:1 text contrast and 24 × 24 px touch targets (theme requirements). So on a real store, findings mostly come from what was added on top: brand colors, custom sections, apps and content. Automated scans also miss a lot. In our demo audit, axe flagged 13 of the 30 findings at least partly. The other 17 needed a keyboard and a screen reader.
The six findings scanners catch
1. Low-contrast text (1.4.3 Contrast (Minimum), AA)
Where: color settings (config/settings_schema.json, the theme editor's color schemes) and assets/base.css.
Fix it at the token level, not element by element. In the demo store, muted text, sale badges, compare-at prices and footer links all came from a few CSS custom properties. Changing those fixed every instance without changing the look of the brand:
:root {
--muted: #636363; /* was #9b9b9b: 2.8:1 on white, now 6.0:1 */
--sale: #a8432a; /* sale badge behind white text: was #e0876a (2.7:1), now 6.0:1 */
}Watch for text dimmed with transparency. Dawn-derived themes often render secondary text as something like rgba(var(--color-foreground), 0.75). On a custom light color scheme, that alpha can push it below 4.5:1 even when the base color passes. Check text over hero images separately: scanners usually mark it "needs review" rather than a failure. In the demo, a gradient overlay behind slide text took the worst case from 1.4:1 to 7.2:1.
2. Missing or useless alt text (1.1.1 Non-text Content, A)
Where: image output in snippets (snippets/card-product.liquid, snippets/product-media-gallery.liquid) and alt text entered in the admin.
Split this into two jobs. The template must output whatever alt text the merchant entered, and must mark purely decorative images with an empty alt. The content (writing alt text for hundreds of product images) is usually the merchant's job unless it's quoted.
{%- comment -%} Product media: use the merchant's alt, fall back to the product title {%- endcomment -%}
{%- assign media_alt = media.alt | default: product.title -%}
{{ media | image_url: width: 1200 | image_tag: alt: media_alt, loading: 'lazy' }}
{%- comment -%} Card image next to a linked product title: decorative {%- endcomment -%}
{{ product.featured_media | image_url: width: 600 | image_tag: alt: '', loading: 'lazy' }}Shopify's docs show image_tag taking an alt value but don't spell out the empty case, so check the rendered HTML for alt="" after the change. If your setup doesn't output it, write the <img> tag yourself with src="{{ product.featured_media | image_url: width: 600 }}", alt="" and explicit width and height.
File names as alt text (IMG_2041.jpg) are a common finding too. They come from imports, so fix the template fallback and give the merchant a list.
3. Empty buttons and links (4.1.2 Name, Role, Value, A; 2.4.4 Link Purpose, A)
Where: sections/header.liquid (search, account, cart, menu icons), social links in sections/footer.liquid, pagination, quick-add buttons in product cards.
Icon-only controls need a text name, and the icon itself should be hidden from assistive tech:
<button type="button" class="header__icon header__icon--cart" aria-controls="CartDrawer">
<svg aria-hidden="true" focusable="false">…</svg>
<span class="visually-hidden">Cart, {{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }}</span>
</button>Two details from the demo fixes:
- Use a real
<button>for anything that opens something. Many custom headers use a<div>or<span>with a click handler, which also fails 2.1.1 Keyboard. - For quick-add buttons repeated in a grid, include the product name ("Quick add Ridgeline 40L Pack to cart"). Otherwise a screen reader's buttons list shows 24 identical "Quick add" entries.
4. Missing form labels (1.3.1, 3.3.2 Labels or Instructions, 4.1.2, A)
Where: newsletter forms (footer and popups), search, collection sort, quantity inputs.
Placeholder text is not a label. Give every field a <label> tied to its id. It can be visually hidden only where the purpose is obvious from context, like a search box next to a search button.
<label for="NewsletterForm-{{ section.id }}">Email address</label>
<input type="email" id="NewsletterForm-{{ section.id }}" name="contact[email]" autocomplete="email" required>The collection sort dropdown is a frequent miss: <select> with a "Sort by" placeholder option and no label. Add <label for="SortBy">Sort by</label>.
5. Missing page language (3.1.1 Language of Page, A)
Where: layout/theme.liquid.
<html lang="{{ request.locale.iso_code }}">Dawn and Horizon already do this. Custom and older themes sometimes don't, and some apps render whole pages through their own layout.
6. Zoom disabled (1.4.4 Resize Text, AA)
Not in WebAIM's top six, but common on older themes: maximum-scale=1, user-scalable=no in the viewport tag. Remove both. <meta name="viewport" content="width=device-width, initial-scale=1"> is enough.
The findings only manual testing catches
7. Focus indicator removed (2.4.7 Focus Visible, AA)
Where: assets/base.css, or a custom stylesheet with *:focus { outline: none; }.
Delete the reset and add a focus style that's visible on both light and dark sections. A two-tone ring handles both:
:focus-visible {
outline: 3px solid #1d4ed8;
outline-offset: 2px;
box-shadow: 0 0 0 5px #fff;
}Custom controls built from visually hidden inputs (swatches, size buttons, star ratings) need the ring drawn on the visible label: .swatch-input:focus-visible + .swatch { outline: … }.
8. Mega menu opens on hover only (2.1.1 Keyboard, A; 1.4.13 Content on Hover or Focus, AA)
Where: sections/header.liquid and the header JS.
Turn the top-level item into a disclosure button, following the APG disclosure navigation example:
<li class="has-mega">
<button type="button" aria-expanded="false" aria-controls="mega-shop">Shop</button>
<div id="mega-shop" class="mega">
<a href="/collections/all">Shop all gear</a>
…
</div>
</li>Enter or Space toggles aria-expanded. Escape closes the panel and returns focus to the button. Focus leaving the item closes it. Hover can still open it, with a short close delay so the pointer can travel. If "Shop" used to be a link, make that destination the first link in the panel.
9. Auto-rotating carousel with no pause (2.2.2 Pause, Stop, Hide, A)
Where: sections/slideshow.liquid and the slider component JS.
Following the APG carousel pattern: put a visible pause/play button first among the controls, stop rotation when keyboard focus enters the carousel, make hidden slides visibility: hidden so their links leave the tab order, and don't autoplay at all under prefers-reduced-motion. Give the dots real button names and at least 24 × 24 px targets (2.5.8 Target Size (Minimum); the Theme Store requires the same minimum).
10. Popups, search overlays and cart drawers that ignore focus (2.1.1, 2.4.3 Focus Order, 4.1.2, A)
Where: snippets/cart-drawer.liquid, the search snippet, popup app embeds.
They need the same fix: a modal dialog with a name, focus moved in on open, the page behind made inert, Escape to close, and focus returned on close. A native <dialog> opened with showModal() does most of it. The full walkthrough with code is in our cart drawer and modal focus guide.
11. Variant pickers built from <div>s (2.1.1, 4.1.2, 1.4.1 Use of Color, A)
Where: snippets/product-variant-picker.liquid and its option snippets.
Color and size options must be a named group of radio buttons, with the selected state visible without relying on color and announced by screen readers. Sold-out options need to say so in text. Details and code are in our swatches guide.
12. "Added to cart" is silent (4.1.3 Status Messages, AA)
Where: the product form JS and cart drawer or notification.
Sighted users see the drawer slide in. Screen reader users hear nothing unless you move focus to a confirmation or put it in a live region. Include the product and variant: "Added to your cart: Ridgeline 40L Pack, Slate / L. Cart has 1 item." The same applies to collection filters: announce the new result count in a role="status" element.
Structure: skip link, landmarks and headings
Three quick fixes in layout/theme.liquid and section templates that improve every page:
- A skip link as the first focusable element, pointing at
<main id="MainContent" tabindex="-1">(2.4.1 Bypass Blocks, A). - Landmarks:
<header>,<nav aria-label="Main">,<main>,<footer>, plusaria-labelon secondary navs like breadcrumbs. - One
<h1>per page and no skipped levels. Section titles styled as<div>s or jumping from<h3>to<h5>in the footer are common findings under 1.3.1. Change the element, keep the visual size with a class.
Findings that aren't theme code
Two categories look the same in a browser but aren't fixed in the theme:
- App code. Reviews widgets, popups, chat and upsell apps inject their own markup. The options are app settings, a theme-side override if the app allows one, a different app, or a ticket to the vendor. In the demo, the reviews widget's form and contrast were fixed through overrides; on a real store that depends on the app.
- Checkout. Checkout is Shopify-hosted on every plan. On non-Plus stores we report checkout issues to Shopify rather than fix them. On Shopify Plus, custom checkout extensions can be in scope. Our Shopify checkout guide explains what that means in practice.
How to check your fixes before the retest
- Run axe (or Lighthouse, which uses a subset of axe's rules) on every changed template, in each state you changed: menu open, drawer open, popup open, filter applied.
- Do a keyboard-only pass: Tab, Shift+Tab, Enter, Space, arrow keys and Escape through each changed component.
- Do a screen reader pass on the same path. VoiceOver on macOS and iOS covers most shoppers; add NVDA on Windows when the audit used it.
- Zoom to 400% and check that content reflows at 320 CSS px wide without sideways scrolling (1.4.10 Reflow).
- Run a regression pass: add to cart, the path to checkout, search, forms.
None of this replaces the auditor's retest. It just makes the retest boring, which is the goal.
Sources
- WebAIM, The WebAIM Million (2026)
- Shopify, Theme Store requirements and theme accessibility best practices
- Shopify,
image_tagfilter andrequest.locale - W3C WAI, Understanding WCAG 2.2
- W3C WAI, APG: Disclosure navigation menu, Carousel pattern
Seeing these in a client's audit? Send us one client's audit findings. We review the findings and the code, then quote a fixed price for an agreed list of fixes. Your team retests. If the agreed fixes fail your retest, you don't pay for them. Send findings.
Want us to fix this in your theme?
We fix accessibility issues like this in your theme code, on a duplicate theme, with a before/after report for every issue. Fixed prices from $450.