Executive summary
Threshwise tested 6 pages of demo-store.example (SAMPLE) (shopify, theme Dawn-demo). Automated checks found 194 failing element instances, which trace back to 41 probable root causes, of which 6 critical and 13 high priority. 9 sit in shared theme components, so one change fixes every page they appear on.
Automated testing (axe-core plus keyboard heuristics) typically detects only part of all WCAG issues. Criteria such as meaningful alt text, logical reading order, error messaging, captions and overall screen-reader usability need human review. Use the manual checklist to complete the evaluation. This report is technical testing evidence, not legal advice and not a certification of compliance with the ADA, EAA, AODA or any law. *The health indicator tracks progress between scans; it is not a compliance score.
Standards tested
WCAG 2.2 Level A and AA success criteria that can be tested automatically (via axe-core 4.13.0), plus best-practice rules (marked separately) and keyboard-navigation heuristics (Tab traversal, focus visibility, focus traps, unreachable controls). Criteria failed: 1.1.1, 1.4.3, 2.1.1, 2.4.2, 2.4.3, 2.4.4, 2.4.7, 2.5.8, 3.1.1, 4.1.2.
Pages tested
| Template | Pages found | Not deep-scanned (same template) |
|---|---|---|
| home | 1 | 0 |
| collection | 1 | 0 |
| contact | 1 | 0 |
| cart | 1 | 0 |
| page | 2 | 0 |
| product | 3 | 1 |
Could not test: https://demo-store.example/pages/about (http_404); https://demo-store.example/pages/faq (http_404)
What to fix first
1. Button has no accessible name
4.1.2 Name, Role, Value — 6 pages, 6 instances — Theme / shared component · component: header
Why it matters: A screen reader announces just "button"; voice-control users cannot say its name to click it. Affects screen-reader users, voice-control users.
Fix once: this is one root cause repeated on 6 pages — fix the shared theme component, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Give icon-only buttons an aria-label describing the action.
- Or include visually-hidden text inside the button.
- Mark the decorative <svg> with aria-hidden="true" focusable="false".
<button class="header__icon header__icon--menu" onclick="document.getElementById('menu-drawer').classList.toggle('is-open')"><svg width="20" height="20" class="icon icon-hamburger"><path d="M0 4h20M0 10h20M0 16h20" stroke="#000"></path></svg></button>.header__icon--menu
https://demo-store.example/
<button class="header__icon header__icon--menu" onclick="document.getElementById('menu-drawer').classList.toggle('is-open')" aria-label="Open menu"> … </button>Platform pattern<button type="button" aria-label="{{ 'general.menu.open' | t | default: 'Open menu' }}">{% render 'icon-hamburger' %}</button>Generic before/after
<button class="header__icon--menu"><svg>…</svg></button> → <button class="header__icon--menu" aria-label="Open menu" aria-expanded="false" aria-controls="menu-drawer"><svg aria-hidden="true" focusable="false">…</svg></button>
All affected locations (6)
| https://demo-store.example/ | .header__icon--menu |
| https://demo-store.example/cart | .header__icon--menu |
| https://demo-store.example/products/linen-shirt | .header__icon--menu |
| https://demo-store.example/pages/contact | .header__icon--menu |
| https://demo-store.example/collections/all | .header__icon--menu |
| https://demo-store.example/products/linen-trousers | .header__icon--menu |
Rule reference: button-name
2. Image has no text alternative
1.1.1 Non-text Content — 6 pages, 6 instances — Theme / shared component · component: header
Why it matters: Screen readers announce the file name or just "image", so the meaning (product, logo, chart) is lost. Affects screen-reader users.
Automation can detect a missing alt but cannot judge if alt text is meaningful. Review each image: decorative → alt="", informative → describe purpose.
Fix once: this is one root cause repeated on 6 pages — fix the shared theme component, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add an alt attribute describing the image purpose (not "image of…").
- If purely decorative, use alt="" so it is skipped.
- For links that contain only an image, the alt must describe the link destination.
<img src="/logo-demo-store.png" width="120" height="30">
img[src$="logo-demo-store.png"]
https://demo-store.example/
<img src="/logo-demo-store.png" width="120" height="30" alt="Logo demo store"> <!-- or alt="" if decorative -->Platform pattern
{{ product.featured_image | image_url: width: 600 | image_tag: alt: product.featured_image.alt | default: product.title }}Generic before/after
<img src="/blue-linen-shirt.jpg"> → <img src="/blue-linen-shirt.jpg" alt="Blue linen shirt, front view">
All affected locations (6)
| https://demo-store.example/ | img[src$="logo-demo-store.png"] |
| https://demo-store.example/cart | img |
| https://demo-store.example/products/linen-shirt | img[src$="logo-demo-store.png"] |
| https://demo-store.example/pages/contact | img |
| https://demo-store.example/collections/all | img[src$="logo-demo-store.png"] |
| https://demo-store.example/products/linen-trousers | img[src$="logo-demo-store.png"] |
Rule reference: image-alt
3. Link has no discernible text
2.4.4 Link Purpose (In Context) · 4.1.2 Name, Role, Value — 6 pages, 12 instances — Theme / shared component · component: footer
Why it matters: Screen readers announce "link" or read out the URL; users cannot tell where it goes. Affects screen-reader users, voice-control users.
Fix once: this is one root cause repeated on 6 pages — fix the shared theme component, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add text inside the link, or an aria-label for icon-only links (e.g. social icons).
- If the link wraps an image, give the image alt text describing the destination.
<a href="https://instagram.com/demo" class="list-social__link"><svg class="icon icon-instagram" width="18" height="18"><rect width="18" height="18"></rect></svg></a>
li:nth-child(1) > .list-social__link[href$="demo"]
https://demo-store.example/
<a href="https://instagram.com/demo" class="list-social__link" aria-label="Instagram"> … </a>Platform pattern
<a href="{{ settings.social_instagram_link }}" aria-label="Instagram">{% render 'icon-instagram' %}</a>Generic before/after
<a href="https://instagram.com/brand"><svg>…</svg></a> → <a href="https://instagram.com/brand" aria-label="Instagram"><svg aria-hidden="true" focusable="false">…</svg></a>
All affected locations (12)
| https://demo-store.example/ | li:nth-child(1) > .list-social__link[href$="demo"] |
| https://demo-store.example/ | li:nth-child(2) > .list-social__link[href$="demo"] |
| https://demo-store.example/cart | li:nth-child(1) > .list-social__link[href$="demo"] |
| https://demo-store.example/cart | li:nth-child(2) > .list-social__link[href$="demo"] |
| https://demo-store.example/products/linen-shirt | li:nth-child(1) > .list-social__link[href$="demo"] |
| https://demo-store.example/products/linen-shirt | li:nth-child(2) > .list-social__link[href$="demo"] |
| https://demo-store.example/pages/contact | li:nth-child(1) > .list-social__link[href$="demo"] |
| https://demo-store.example/pages/contact | li:nth-child(2) > .list-social__link[href$="demo"] |
| https://demo-store.example/collections/all | li:nth-child(1) > .list-social__link[href$="demo"] |
| https://demo-store.example/collections/all | li:nth-child(2) > .list-social__link[href$="demo"] |
| https://demo-store.example/products/linen-trousers | li:nth-child(1) > .list-social__link[href$="demo"] |
| https://demo-store.example/products/linen-trousers | li:nth-child(2) > .list-social__link[href$="demo"] |
Rule reference: link-name
4. Link has no discernible text
2.4.4 Link Purpose (In Context) · 4.1.2 Name, Role, Value — 6 pages, 6 instances — Theme / shared component · component: header
Why it matters: Screen readers announce "link" or read out the URL; users cannot tell where it goes. Affects screen-reader users, voice-control users.
Fix once: this is one root cause repeated on 6 pages — fix the shared theme component, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add text inside the link, or an aria-label for icon-only links (e.g. social icons).
- If the link wraps an image, give the image alt text describing the destination.
<a href="/" class="header__heading-link"><img src="/logo-demo-store.png" width="120" height="30"></a>
.header__heading-link
https://demo-store.example/
<a href="/" class="header__heading-link" aria-label="Describe destination"> … </a>Platform pattern
<a href="{{ settings.social_instagram_link }}" aria-label="Instagram">{% render 'icon-instagram' %}</a>Generic before/after
<a href="https://instagram.com/brand"><svg>…</svg></a> → <a href="https://instagram.com/brand" aria-label="Instagram"><svg aria-hidden="true" focusable="false">…</svg></a>
All affected locations (6)
| https://demo-store.example/ | .header__heading-link |
| https://demo-store.example/cart | .header__heading-link |
| https://demo-store.example/products/linen-shirt | .header__heading-link |
| https://demo-store.example/pages/contact | .header__heading-link |
| https://demo-store.example/collections/all | .header__heading-link |
| https://demo-store.example/products/linen-trousers | .header__heading-link |
Rule reference: link-name
5. Link has no discernible text
2.4.4 Link Purpose (In Context) · 4.1.2 Name, Role, Value — 6 pages, 6 instances — Theme / shared component · component: header
Why it matters: Screen readers announce "link" or read out the URL; users cannot tell where it goes. Affects screen-reader users, voice-control users.
Fix once: this is one root cause repeated on 6 pages — fix the shared theme component, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add text inside the link, or an aria-label for icon-only links (e.g. social icons).
- If the link wraps an image, give the image alt text describing the destination.
<a href="/cart" class="header__icon header__icon--cart"><svg class="icon icon-cart" width="20" height="20"><circle cx="10" cy="10" r="8"></circle></svg></a>
.header__icon--cart
https://demo-store.example/
<a href="/cart" class="header__icon header__icon--cart" aria-label="Open cart"> … </a>Platform pattern
<a href="{{ settings.social_instagram_link }}" aria-label="Instagram">{% render 'icon-instagram' %}</a>Generic before/after
<a href="https://instagram.com/brand"><svg>…</svg></a> → <a href="https://instagram.com/brand" aria-label="Instagram"><svg aria-hidden="true" focusable="false">…</svg></a>
All affected locations (6)
| https://demo-store.example/ | .header__icon--cart |
| https://demo-store.example/cart | .header__icon--cart |
| https://demo-store.example/products/linen-shirt | .header__icon--cart |
| https://demo-store.example/pages/contact | .header__icon--cart |
| https://demo-store.example/collections/all | .header__icon--cart |
| https://demo-store.example/products/linen-trousers | .header__icon--cart |
Rule reference: link-name
6. Select menu has no label
4.1.2 Name, Role, Value — 2 pages, 2 instances — Page template · component: main
Why it matters: The purpose of the dropdown (size, colour, sort order) is not announced. Affects screen-reader users, voice-control users.
Fix once: this is one root cause repeated on 2 pages — fix the shared template, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Associate a <label for> with the select, or add aria-label.
<select name="quantity"><option>1</option><option>2</option></select>
select
https://demo-store.example/products/linen-shirt
<select name="quantity" aria-label="Quantity"> … </select>
Generic before/after
<select name="sort_by">…</select> → <label for="SortBy">Sort by</label> <select id="SortBy" name="sort_by">…</select>
All affected locations (2)
| https://demo-store.example/products/linen-shirt | select |
| https://demo-store.example/products/linen-trousers | select |
Rule reference: select-name
7. Form field has no label
4.1.2 Name, Role, Value — 1 page, 1 instance — Single page
Why it matters: Screen-reader users hear "edit text" with no idea what to type; placeholder text disappears on input and is not a reliable label. Affects screen-reader users, users with cognitive disabilities, voice-control users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add a <label for="id"> associated with the field (visually hidden if design requires).
- Or aria-label / aria-labelledby when a visible label is impossible.
- Do not rely on placeholder alone.
<input type="number" value="1" name="updates[]">
input[type="number"]
https://demo-store.example/cart
<label for="updates[]-input">updates[]</label> <input type="number" value="1" name="updates[]" id="updates[]-input">Platform pattern
<label for="NewsletterForm-{{ section.id }}" class="visually-hidden">{{ 'newsletter.label' | t }}</label>
<input id="NewsletterForm-{{ section.id }}" type="email" name="contact[email]" autocomplete="email">Generic before/after
<input type="email" name="contact[email]" placeholder="Email"> → <label for="NewsletterEmail" class="visually-hidden">Email address</label> <input id="NewsletterEmail" type="email" name="contact[email]" placeholder="Email" autocomplete="email">
Rule reference: label
8. Image has no text alternative
1.1.1 Non-text Content — 2 pages, 4 instances — Content (editor) · component: main
Why it matters: Screen readers announce the file name or just "image", so the meaning (product, logo, chart) is lost. Affects screen-reader users.
Automation can detect a missing alt but cannot judge if alt text is meaningful. Review each image: decorative → alt="", informative → describe purpose.
Fix once: this is one root cause repeated on 2 pages — fix the shared template, not each page.
Where: Content fixes (image alt text, link text in rich text) are made in the Shopify admin, not the theme: Products → image → "Add alt text", or Online Store → Navigation.
- Add an alt attribute describing the image purpose (not "image of…").
- If purely decorative, use alt="" so it is skipped.
- For links that contain only an image, the alt must describe the link destination.
<img src="/files/linen-shirt-front_1200x.jpg" width="400" height="400">
img[width="400"]
https://demo-store.example/products/linen-shirt
<img src="/files/linen-shirt-front_1200x.jpg" width="400" height="400" alt="Linen shirt front 1200x"> <!-- or alt="" if decorative -->Platform pattern
{{ product.featured_image | image_url: width: 600 | image_tag: alt: product.featured_image.alt | default: product.title }}Generic before/after
<img src="/blue-linen-shirt.jpg"> → <img src="/blue-linen-shirt.jpg" alt="Blue linen shirt, front view">
All affected locations (4)
| https://demo-store.example/products/linen-shirt | img[width="400"] |
| https://demo-store.example/products/linen-shirt | img[src$="care-label.png"] |
| https://demo-store.example/products/linen-trousers | img[width="400"] |
| https://demo-store.example/products/linen-trousers | img[src$="care-label.png"] |
Rule reference: image-alt
9. Form field has no label
4.1.2 Name, Role, Value — 1 page, 1 instance — Page template · component: contact-form
Why it matters: Screen-reader users hear "edit text" with no idea what to type; placeholder text disappears on input and is not a reliable label. Affects screen-reader users, users with cognitive disabilities, voice-control users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add a <label for="id"> associated with the field (visually hidden if design requires).
- Or aria-label / aria-labelledby when a visible label is impossible.
- Do not rely on placeholder alone.
<textarea name="contact[body]"></textarea>
textarea
https://demo-store.example/pages/contact
<label for="contact[body]-input">contact[body]</label> <textarea name="contact[body]" id="contact[body]-input">Platform pattern
<label for="NewsletterForm-{{ section.id }}" class="visually-hidden">{{ 'newsletter.label' | t }}</label>
<input id="NewsletterForm-{{ section.id }}" type="email" name="contact[email]" autocomplete="email">Generic before/after
<input type="email" name="contact[email]" placeholder="Email"> → <label for="NewsletterEmail" class="visually-hidden">Email address</label> <input id="NewsletterEmail" type="email" name="contact[email]" placeholder="Email" autocomplete="email">
Rule reference: label
10. Touch target is too small (WCAG 2.2)
2.5.8 Target Size (Minimum) — 6 pages, 12 instances — Theme / shared component · component: header
Why it matters: Small, tightly packed controls are hard to hit for users with tremors or on mobile. Affects users with motor impairments.
Fix once: this is one root cause repeated on 6 pages — fix the shared theme component, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Make interactive targets at least 24×24 CSS px, or space them so a 24px circle around each does not overlap another.
<a href="/collections/all">Shop</a>
a[href$="all"]
https://demo-store.example/
a[href$="all"] { min-width: 24px; min-height: 24px; }Generic before/after
.swatch { width: 16px; height: 16px; }
→
.swatch { min-width: 24px; min-height: 24px; }All affected locations (12)
| https://demo-store.example/ | a[href$="all"] |
| https://demo-store.example/ | a[href$="contact"] |
| https://demo-store.example/cart | a[href$="all"] |
| https://demo-store.example/cart | a[href$="contact"] |
| https://demo-store.example/products/linen-shirt | a[href$="all"] |
| https://demo-store.example/products/linen-shirt | a[href$="contact"] |
| https://demo-store.example/pages/contact | a[href$="all"] |
| https://demo-store.example/pages/contact | a[href$="contact"] |
| https://demo-store.example/collections/all | a[href$="all"] |
| https://demo-store.example/collections/all | a[href$="contact"] |
| https://demo-store.example/products/linen-trousers | a[href$="all"] |
| https://demo-store.example/products/linen-trousers | a[href$="contact"] |
Rule reference: target-size
11. Select menu has no label
4.1.2 Name, Role, Value — 1 page, 1 instance — Single page
Why it matters: The purpose of the dropdown (size, colour, sort order) is not announced. Affects screen-reader users, voice-control users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Associate a <label for> with the select, or add aria-label.
<select name="sort_by"><option>Featured</option></select>
select
https://demo-store.example/collections/all
<select name="sort_by" aria-label="Sort by"> … </select>
Generic before/after
<select name="sort_by">…</select> → <label for="SortBy">Sort by</label> <select id="SortBy" name="sort_by">…</select>
Rule reference: select-name
12. Text colour contrast is too low
1.4.3 Contrast (Minimum) — 6 pages, 6 instances — Theme / shared component · component: footer
Why it matters: Low-contrast text is hard or impossible to read for people with low vision, older users and anyone on a phone in sunlight. Affects low-vision users, colour-blind users.
Fix once: this is one root cause repeated on 6 pages — fix the shared theme component, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Raise contrast to at least 4.5:1 for normal text, 3:1 for large text (≥24px or ≥18.66px bold).
- Change the colour in the theme/design tokens (one change fixes every page).
<p class="muted">© Demo Store</p>
.muted
https://demo-store.example/
/* #b5b5b5 on #f6f6f6 = 1.89:1, needs 4.5:1 */
.muted { color: #696969; } /* ≈5.08:1 */Platform patternTheme settings → Colors: adjust the scheme used by this section, or override in assets/base.css.
Generic before/after
.price--sale { color: #ff8a8a; } /* 2.6:1 on white */
→
.price--sale { color: #c62828; } /* 5.6:1 on white */All affected locations (6)
| https://demo-store.example/ | .muted |
| https://demo-store.example/cart | p |
| https://demo-store.example/products/linen-shirt | #shopify-section-footer > .muted |
| https://demo-store.example/pages/contact | p |
| https://demo-store.example/collections/all | p |
| https://demo-store.example/products/linen-trousers | #shopify-section-footer > .muted |
Rule reference: color-contrast
13. Image has no text alternative
1.1.1 Non-text Content — 2 pages, 4 instances — Content (editor)
Why it matters: Screen readers announce the file name or just "image", so the meaning (product, logo, chart) is lost. Affects screen-reader users.
Automation can detect a missing alt but cannot judge if alt text is meaningful. Review each image: decorative → alt="", informative → describe purpose.
Fix once: this is one root cause repeated on 2 pages — fix the shared template, not each page.
Where: Content fixes (image alt text, link text in rich text) are made in the Shopify admin, not the theme: Products → image → "Add alt text", or Online Store → Navigation.
- Add an alt attribute describing the image purpose (not "image of…").
- If purely decorative, use alt="" so it is skipped.
- For links that contain only an image, the alt must describe the link destination.
<img src="/files/hero_2000x.jpg" width="800" height="300">
img[src$="hero_2000x.jpg"]
https://demo-store.example/
<img src="/files/hero_2000x.jpg" width="800" height="300" alt="Hero 2000x"> <!-- or alt="" if decorative -->Platform pattern
{{ product.featured_image | image_url: width: 600 | image_tag: alt: product.featured_image.alt | default: product.title }}Generic before/after
<img src="/blue-linen-shirt.jpg"> → <img src="/blue-linen-shirt.jpg" alt="Blue linen shirt, front view">
All affected locations (4)
| https://demo-store.example/ | img[src$="hero_2000x.jpg"] |
| https://demo-store.example/collections/all | img[src$="linen-shirt_400x.jpg"] |
| https://demo-store.example/collections/all | img[src$="linen-trousers_400x.jpg"] |
| https://demo-store.example/collections/all | img[src$="linen-dress_400x.jpg"] |
Rule reference: image-alt
14. ARIA button/link/menuitem has no name
4.1.2 Name, Role, Value — 2 pages, 2 instances — Page template · component: main
Why it matters: The control is announced without its action. Affects screen-reader users, voice-control users.
Fix once: this is one root cause repeated on 2 pages — fix the shared template, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add text content or aria-label.
<div class="swatch" role="button" style="background:#c00" onclick="this.classList.add('active')"></div>.swatch
https://demo-store.example/products/linen-shirt
<div class="swatch" role="button" style="background:#c00" onclick="this.classList.add('active')" aria-label="Previous">Generic before/after
<div role="button"><i class="icon-close"></i></div> → <button type="button" aria-label="Close"><i class="icon-close" aria-hidden="true"></i></button>
All affected locations (2)
| https://demo-store.example/products/linen-shirt | .swatch |
| https://demo-store.example/products/linen-trousers | .swatch |
Rule reference: aria-command-name
15. Image has no text alternative
1.1.1 Non-text Content — 1 page, 3 instances — Content (editor) · component: product-card
Why it matters: Screen readers announce the file name or just "image", so the meaning (product, logo, chart) is lost. Affects screen-reader users.
Automation can detect a missing alt but cannot judge if alt text is meaningful. Review each image: decorative → alt="", informative → describe purpose.
Fix once: single location.
Where: Content fixes (image alt text, link text in rich text) are made in the Shopify admin, not the theme: Products → image → "Add alt text", or Online Store → Navigation.
- Add an alt attribute describing the image purpose (not "image of…").
- If purely decorative, use alt="" so it is skipped.
- For links that contain only an image, the alt must describe the link destination.
<img src="/files/linen-shirt_600x.jpg" width="200" height="200">
img[src$="linen-shirt_600x.jpg"]
https://demo-store.example/
<img src="/files/linen-shirt_600x.jpg" width="200" height="200" alt="Linen shirt 600x"> <!-- or alt="" if decorative -->Platform pattern
{{ product.featured_image | image_url: width: 600 | image_tag: alt: product.featured_image.alt | default: product.title }}Generic before/after
<img src="/blue-linen-shirt.jpg"> → <img src="/blue-linen-shirt.jpg" alt="Blue linen shirt, front view">
All affected locations (3)
| https://demo-store.example/ | img[src$="linen-shirt_600x.jpg"] |
| https://demo-store.example/ | img[src$="linen-trousers_600x.jpg"] |
| https://demo-store.example/ | img[src$="linen-dress_600x.jpg"] |
Rule reference: image-alt
16. Link has no discernible text
2.4.4 Link Purpose (In Context) · 4.1.2 Name, Role, Value — 1 page, 3 instances — Page template · component: product-card
Why it matters: Screen readers announce "link" or read out the URL; users cannot tell where it goes. Affects screen-reader users, voice-control users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add text inside the link, or an aria-label for icon-only links (e.g. social icons).
- If the link wraps an image, give the image alt text describing the destination.
<a href="/products/linen-shirt" class="card__link"><img src="/files/linen-shirt_600x.jpg" width="200" height="200"></a>
a[href$="linen-shirt"]
https://demo-store.example/
<a href="/products/linen-shirt" class="card__link" aria-label="Describe destination"> … </a>Platform pattern
<a href="{{ settings.social_instagram_link }}" aria-label="Instagram">{% render 'icon-instagram' %}</a>Generic before/after
<a href="https://instagram.com/brand"><svg>…</svg></a> → <a href="https://instagram.com/brand" aria-label="Instagram"><svg aria-hidden="true" focusable="false">…</svg></a>
All affected locations (3)
| https://demo-store.example/ | a[href$="linen-shirt"] |
| https://demo-store.example/ | a[href$="linen-trousers"] |
| https://demo-store.example/ | a[href$="linen-dress"] |
Rule reference: link-name
17. Keyboard focus is not visible
2.4.7 Focus Visible — 6 pages, 54 instances — Theme / shared component
Why it matters: Sighted keyboard users cannot see where they are on the page. Affects keyboard-only users, low-vision users.
Heuristic: we compared the element style before/after focus. Confirm by tabbing through the page.
Fix once: this is one root cause repeated on 6 pages — fix the shared theme component, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Do not remove outlines (outline: none) without a replacement.
- Add a clear :focus-visible style with ≥3:1 contrast.
<button class="header__icon header__icon--menu" onclick="document.getElementById('menu-drawer').classList.toggle('is-open')">#shopify-section-header > button.header__icon.header__icon--menu
https://demo-store.example/
#shopify-section-header > button.header__icon.header__icon--menu:focus-visible { outline: 3px solid currentColor; outline-offset: 2px; }Generic before/after
a:focus, button:focus { outline: none; }
→
:focus-visible { outline: 3px solid #1a5fb4; outline-offset: 2px; }All affected locations (54)
| https://demo-store.example/ | #shopify-section-header > button.header__icon.header__icon--menu |
| https://demo-store.example/ | #shopify-section-header > a.header__heading-link |
| https://demo-store.example/ | nav > ul.list-menu > li:nth-of-type(1) > a |
| https://demo-store.example/ | nav > ul.list-menu > li:nth-of-type(2) > a |
| https://demo-store.example/ | #shopify-section-header > a.header__icon.header__icon--cart |
| https://demo-store.example/ | #MainContent > div.product-grid > div.card-wrapper.product-card > a.card__link |
| https://demo-store.example/cart | #shopify-section-header > button.header__icon.header__icon--menu |
| https://demo-store.example/cart | #shopify-section-header > a.header__heading-link |
| https://demo-store.example/cart | nav > ul.list-menu > li:nth-of-type(1) > a |
| https://demo-store.example/cart | nav > ul.list-menu > li:nth-of-type(2) > a |
| https://demo-store.example/cart | #shopify-section-header > a.header__icon.header__icon--cart |
| https://demo-store.example/cart | tbody > tr:nth-of-type(2) > td:nth-of-type(2) > input |
| https://demo-store.example/cart | #MainContent > button.btn |
| https://demo-store.example/cart | #shopify-section-footer > form.newsletter-form > input |
| https://demo-store.example/cart | #shopify-section-footer > form.newsletter-form > button.btn |
| https://demo-store.example/cart | #shopify-section-footer > ul.list-social > li:nth-of-type(1) > a.list-social__link |
| https://demo-store.example/cart | #shopify-section-footer > ul.list-social > li:nth-of-type(2) > a.list-social__link |
| https://demo-store.example/products/linen-shirt | #shopify-section-header > button.header__icon.header__icon--menu |
| https://demo-store.example/products/linen-shirt | #shopify-section-header > a.header__heading-link |
| https://demo-store.example/products/linen-shirt | nav > ul.list-menu > li:nth-of-type(1) > a |
| https://demo-store.example/products/linen-shirt | nav > ul.list-menu > li:nth-of-type(2) > a |
| https://demo-store.example/products/linen-shirt | #shopify-section-header > a.header__icon.header__icon--cart |
| https://demo-store.example/products/linen-shirt | #shopify-section-template--1234__main > button.btn.product-form__submit |
| https://demo-store.example/products/linen-shirt | #shopify-section-footer > form.newsletter-form > input |
| https://demo-store.example/products/linen-shirt | #shopify-section-footer > form.newsletter-form > button.btn |
| https://demo-store.example/products/linen-shirt | #shopify-section-footer > ul.list-social > li:nth-of-type(1) > a.list-social__link |
| https://demo-store.example/products/linen-shirt | #shopify-section-footer > ul.list-social > li:nth-of-type(2) > a.list-social__link |
| https://demo-store.example/pages/contact | #shopify-section-header > button.header__icon.header__icon--menu |
| https://demo-store.example/pages/contact | #shopify-section-header > a.header__heading-link |
| https://demo-store.example/pages/contact | nav > ul.list-menu > li:nth-of-type(1) > a |
| https://demo-store.example/pages/contact | nav > ul.list-menu > li:nth-of-type(2) > a |
| https://demo-store.example/pages/contact | #shopify-section-header > a.header__icon.header__icon--cart |
| https://demo-store.example/pages/contact | #MainContent > form.contact-form > input |
| https://demo-store.example/pages/contact | #MainContent > form.contact-form > button.btn |
| https://demo-store.example/pages/contact | #shopify-section-footer > form.newsletter-form > input |
| https://demo-store.example/pages/contact | #shopify-section-footer > form.newsletter-form > button.btn |
| https://demo-store.example/pages/contact | #shopify-section-footer > ul.list-social > li:nth-of-type(1) > a.list-social__link |
| https://demo-store.example/pages/contact | #shopify-section-footer > ul.list-social > li:nth-of-type(2) > a.list-social__link |
| https://demo-store.example/collections/all | #shopify-section-header > button.header__icon.header__icon--menu |
| https://demo-store.example/collections/all | #shopify-section-header > a.header__heading-link |
| https://demo-store.example/collections/all | nav > ul.list-menu > li:nth-of-type(1) > a |
| https://demo-store.example/collections/all | nav > ul.list-menu > li:nth-of-type(2) > a |
| https://demo-store.example/collections/all | #shopify-section-header > a.header__icon.header__icon--cart |
| https://demo-store.example/collections/all | #MainContent > ul.grid > li.grid__item > a |
| https://demo-store.example/products/linen-trousers | #shopify-section-header > button.header__icon.header__icon--menu |
| https://demo-store.example/products/linen-trousers | #shopify-section-header > a.header__heading-link |
| https://demo-store.example/products/linen-trousers | nav > ul.list-menu > li:nth-of-type(1) > a |
| https://demo-store.example/products/linen-trousers | nav > ul.list-menu > li:nth-of-type(2) > a |
| https://demo-store.example/products/linen-trousers | #shopify-section-header > a.header__icon.header__icon--cart |
| https://demo-store.example/products/linen-trousers | #shopify-section-template--1234__main > button.btn.product-form__submit |
| https://demo-store.example/products/linen-trousers | #shopify-section-footer > form.newsletter-form > input |
| https://demo-store.example/products/linen-trousers | #shopify-section-footer > form.newsletter-form > button.btn |
| https://demo-store.example/products/linen-trousers | #shopify-section-footer > ul.list-social > li:nth-of-type(1) > a.list-social__link |
| https://demo-store.example/products/linen-trousers | #shopify-section-footer > ul.list-social > li:nth-of-type(2) > a.list-social__link |
18. Link has no discernible text
2.4.4 Link Purpose (In Context) · 4.1.2 Name, Role, Value — 1 page, 3 instances — Single page
Why it matters: Screen readers announce "link" or read out the URL; users cannot tell where it goes. Affects screen-reader users, voice-control users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add text inside the link, or an aria-label for icon-only links (e.g. social icons).
- If the link wraps an image, give the image alt text describing the destination.
<a href="/products/linen-shirt"><img src="/files/linen-shirt_400x.jpg"></a>
a[href$="linen-shirt"]
https://demo-store.example/collections/all
<a href="/products/linen-shirt" aria-label="Describe destination"> … </a>Platform pattern
<a href="{{ settings.social_instagram_link }}" aria-label="Instagram">{% render 'icon-instagram' %}</a>Generic before/after
<a href="https://instagram.com/brand"><svg>…</svg></a> → <a href="https://instagram.com/brand" aria-label="Instagram"><svg aria-hidden="true" focusable="false">…</svg></a>
All affected locations (3)
| https://demo-store.example/collections/all | a[href$="linen-shirt"] |
| https://demo-store.example/collections/all | a[href$="linen-trousers"] |
| https://demo-store.example/collections/all | a[href$="linen-dress"] |
Rule reference: link-name
19. Page language is not declared
3.1.1 Language of Page — 2 pages, 2 instances — Single page
Why it matters: Screen readers guess the pronunciation language; English text may be read with the wrong voice. Affects screen-reader users.
Fix once: this is one root cause repeated on 2 pages — fix the shared template, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add lang to the <html> element.
<html>
html
https://demo-store.example/products/linen-shirt
<html lang="en"> <!-- use the page's real language code -->Platform pattern
<html lang="{{ request.locale.iso_code }}"> in layout/theme.liquidGeneric before/after
<html> → <html lang="en">
All affected locations (2)
| https://demo-store.example/products/linen-shirt | html |
| https://demo-store.example/products/linen-trousers | html |
Rule reference: html-has-lang
20. Text colour contrast is too low
1.4.3 Contrast (Minimum) — 2 pages, 2 instances — Page template · component: main
Why it matters: Low-contrast text is hard or impossible to read for people with low vision, older users and anyone on a phone in sunlight. Affects low-vision users, colour-blind users.
Fix once: this is one root cause repeated on 2 pages — fix the shared template, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Raise contrast to at least 4.5:1 for normal text, 3:1 for large text (≥24px or ≥18.66px bold).
- Change the colour in the theme/design tokens (one change fixes every page).
<span class="price--sale">$39.00</span>
span
https://demo-store.example/products/linen-shirt
/* #ff9d9d on #ffffff = 1.98:1, needs 4.5:1 */
span { color: #a46363; } /* ≈4.62:1 */Platform patternTheme settings → Colors: adjust the scheme used by this section, or override in assets/base.css.
Generic before/after
.price--sale { color: #ff8a8a; } /* 2.6:1 on white */
→
.price--sale { color: #c62828; } /* 5.6:1 on white */All affected locations (2)
| https://demo-store.example/products/linen-shirt | span |
| https://demo-store.example/products/linen-trousers | span |
Rule reference: color-contrast
21. Text colour contrast is too low
1.4.3 Contrast (Minimum) — 2 pages, 2 instances — Page template · component: main
Why it matters: Low-contrast text is hard or impossible to read for people with low vision, older users and anyone on a phone in sunlight. Affects low-vision users, colour-blind users.
Fix once: this is one root cause repeated on 2 pages — fix the shared template, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Raise contrast to at least 4.5:1 for normal text, 3:1 for large text (≥24px or ≥18.66px bold).
- Change the colour in the theme/design tokens (one change fixes every page).
<s class="muted">$59.00</s>
s
https://demo-store.example/products/linen-shirt
/* #b5b5b5 on #ffffff = 2.05:1, needs 4.5:1 */
s { color: #737373; } /* ≈4.74:1 */Platform patternTheme settings → Colors: adjust the scheme used by this section, or override in assets/base.css.
Generic before/after
.price--sale { color: #ff8a8a; } /* 2.6:1 on white */
→
.price--sale { color: #c62828; } /* 5.6:1 on white */All affected locations (2)
| https://demo-store.example/products/linen-shirt | s |
| https://demo-store.example/products/linen-trousers | s |
Rule reference: color-contrast
22. Page language is not declared
3.1.1 Language of Page — 1 page, 1 instance — Single page
Why it matters: Screen readers guess the pronunciation language; English text may be read with the wrong voice. Affects screen-reader users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add lang to the <html> element.
<html>
html
https://demo-store.example/cart
<html lang="en"> <!-- use the page's real language code -->Platform pattern
<html lang="{{ request.locale.iso_code }}"> in layout/theme.liquidGeneric before/after
<html> → <html lang="en">
Rule reference: html-has-lang
23. Clickable element cannot be reached with the keyboard
2.1.1 Keyboard — 2 pages, 2 instances — Single page
Why it matters: Elements with click handlers but no keyboard focus (div/span "buttons") are unusable without a mouse. Affects keyboard-only users, screen-reader users, voice-control users.
Heuristic: element looks interactive (cursor:pointer / onclick / role) but never received focus while tabbing. Confirm manually.
Fix once: this is one root cause repeated on 2 pages — fix the shared template, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Use a real <button> or <a href>.
- If you must use a div, add role="button", tabindex="0" and Enter/Space key handlers.
<div class="swatch" role="button" style="background:#c00" onclick="this.classList.add('active')"></div>div.swatch
https://demo-store.example/products/linen-shirt
<button type="button" class="swatch" role="button" style="background:#c00" onclick="this.classList.add('active')"></button>Generic before/after
<div class="swatch" onclick="pick('red')">Red</div>
→
<button type="button" class="swatch" onclick="pick('red')" aria-pressed="false">Red</button>All affected locations (2)
| https://demo-store.example/products/linen-shirt | div.swatch |
| https://demo-store.example/products/linen-trousers | div.swatch |
24. Page language is not declared
3.1.1 Language of Page — 1 page, 1 instance — Single page
Why it matters: Screen readers guess the pronunciation language; English text may be read with the wrong voice. Affects screen-reader users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add lang to the <html> element.
<html>
html
https://demo-store.example/pages/contact
<html lang="en"> <!-- use the page's real language code -->Platform pattern
<html lang="{{ request.locale.iso_code }}"> in layout/theme.liquidGeneric before/after
<html> → <html lang="en">
Rule reference: html-has-lang
25. Text colour contrast is too low
1.4.3 Contrast (Minimum) — 1 page, 3 instances — Page template · component: product-card
Why it matters: Low-contrast text is hard or impossible to read for people with low vision, older users and anyone on a phone in sunlight. Affects low-vision users, colour-blind users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Raise contrast to at least 4.5:1 for normal text, 3:1 for large text (≥24px or ≥18.66px bold).
- Change the colour in the theme/design tokens (one change fixes every page).
<p class="price--sale">$39</p>
.card-wrapper.product-card:nth-child(1) > .price--sale
https://demo-store.example/
/* #ff9d9d on #ffffff = 1.98:1, needs 4.5:1 */
.card-wrapper.product-card:nth-child(1) > .price--sale { color: #a46363; } /* ≈4.62:1 */Platform patternTheme settings → Colors: adjust the scheme used by this section, or override in assets/base.css.
Generic before/after
.price--sale { color: #ff8a8a; } /* 2.6:1 on white */
→
.price--sale { color: #c62828; } /* 5.6:1 on white */All affected locations (3)
| https://demo-store.example/ | .card-wrapper.product-card:nth-child(1) > .price--sale |
| https://demo-store.example/ | .card-wrapper.product-card:nth-child(2) > .price--sale |
| https://demo-store.example/ | .card-wrapper.product-card:nth-child(3) > .price--sale |
Rule reference: color-contrast
26. Page has no title
2.4.2 Page Titled — 1 page, 1 instance — Single page
Why it matters: The title is the first thing announced and identifies the tab. Affects screen-reader users, users with cognitive disabilities.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add a unique, descriptive <title>.
<html>
html
https://demo-store.example/
<title>Page topic – Site name</title>
Generic before/after
<head>…</head> → <title>Blue Linen Shirt – Brand</title>
Rule reference: document-title
27. Page language is not declared
3.1.1 Language of Page — 1 page, 1 instance — Single page
Why it matters: Screen readers guess the pronunciation language; English text may be read with the wrong voice. Affects screen-reader users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add lang to the <html> element.
<html>
html
https://demo-store.example/
<html lang="en"> <!-- use the page's real language code -->Platform pattern
<html lang="{{ request.locale.iso_code }}"> in layout/theme.liquidGeneric before/after
<html> → <html lang="en">
Rule reference: html-has-lang
28. Page language is not declared
3.1.1 Language of Page — 1 page, 1 instance — Single page
Why it matters: Screen readers guess the pronunciation language; English text may be read with the wrong voice. Affects screen-reader users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add lang to the <html> element.
<html>
html
https://demo-store.example/collections/all
<html lang="en"> <!-- use the page's real language code -->Platform pattern
<html lang="{{ request.locale.iso_code }}"> in layout/theme.liquidGeneric before/after
<html> → <html lang="en">
Rule reference: html-has-lang
29. Touch target is too small (WCAG 2.2)
2.5.8 Target Size (Minimum) — 1 page, 3 instances — Single page
Why it matters: Small, tightly packed controls are hard to hit for users with tremors or on mobile. Affects users with motor impairments.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Make interactive targets at least 24×24 CSS px, or space them so a 24px circle around each does not overlap another.
<a href="/products/linen-shirt"><img src="/files/linen-shirt_400x.jpg"></a>
a[href$="linen-shirt"]
https://demo-store.example/collections/all
a[href$="linen-shirt"] { min-width: 24px; min-height: 24px; }Generic before/after
.swatch { width: 16px; height: 16px; }
→
.swatch { min-width: 24px; min-height: 24px; }All affected locations (3)
| https://demo-store.example/collections/all | a[href$="linen-shirt"] |
| https://demo-store.example/collections/all | a[href$="linen-trousers"] |
| https://demo-store.example/collections/all | a[href$="linen-dress"] |
Rule reference: target-size
30. Focus moves to hidden or off-screen content
2.4.3 Focus Order — 6 pages, 12 instances — Theme / shared component
Why it matters: Keyboard users tab into closed menus/drawers and lose track of focus. Affects keyboard-only users, screen-reader users.
Heuristic: a focused element was invisible or outside the viewport. Confirm manually.
Fix once: this is one root cause repeated on 6 pages — fix the shared theme component, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- When a drawer/menu is closed, hide it with display:none / hidden / inert, not just transform or opacity.
<a href="/pages/about">
#menu-drawer > a:nth-of-type(1)
https://demo-store.example/
#menu-drawer > a:nth-of-type(1) { visibility: hidden; } /* while closed; or add the inert attribute */Generic before/after
.menu-drawer { transform: translateX(-100%); }
→
.menu-drawer:not([open]) { visibility: hidden; } /* or the inert attribute */All affected locations (12)
| https://demo-store.example/ | #menu-drawer > a:nth-of-type(1) |
| https://demo-store.example/ | #menu-drawer > a:nth-of-type(2) |
| https://demo-store.example/cart | #menu-drawer > a:nth-of-type(1) |
| https://demo-store.example/cart | #menu-drawer > a:nth-of-type(2) |
| https://demo-store.example/products/linen-shirt | #menu-drawer > a:nth-of-type(1) |
| https://demo-store.example/products/linen-shirt | #menu-drawer > a:nth-of-type(2) |
| https://demo-store.example/pages/contact | #menu-drawer > a:nth-of-type(1) |
| https://demo-store.example/pages/contact | #menu-drawer > a:nth-of-type(2) |
| https://demo-store.example/collections/all | #menu-drawer > a:nth-of-type(1) |
| https://demo-store.example/collections/all | #menu-drawer > a:nth-of-type(2) |
| https://demo-store.example/products/linen-trousers | #menu-drawer > a:nth-of-type(1) |
| https://demo-store.example/products/linen-trousers | #menu-drawer > a:nth-of-type(2) |
31. Content is outside landmarks
Not a WCAG failure on its own (best practice) — 2 pages, 14 instances — Page template · component: main
Why it matters: Content not inside header/nav/main/footer can be missed when navigating by landmark. Affects screen-reader users.
Fix once: this is one root cause repeated on 2 pages — fix the shared template, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Place all visible content inside <header>, <nav>, <main>, <aside> or <footer>.
<img src="/files/linen-shirt-front_1200x.jpg" width="400" height="400">
img[width="400"]
https://demo-store.example/products/linen-shirt
<header><div class="announcement">Free shipping</div></header>
All affected locations (14)
| https://demo-store.example/products/linen-shirt | img[width="400"] |
| https://demo-store.example/products/linen-shirt | .h1 |
| https://demo-store.example/products/linen-shirt | #shopify-section-template--1234__main > p |
| https://demo-store.example/products/linen-shirt | legend |
| https://demo-store.example/products/linen-shirt | select |
| https://demo-store.example/products/linen-shirt | h4 |
| https://demo-store.example/products/linen-shirt | .product__description |
| https://demo-store.example/products/linen-trousers | img[width="400"] |
| https://demo-store.example/products/linen-trousers | .h1 |
| https://demo-store.example/products/linen-trousers | #shopify-section-template--1234__main > p |
| https://demo-store.example/products/linen-trousers | legend |
| https://demo-store.example/products/linen-trousers | select |
| https://demo-store.example/products/linen-trousers | h4 |
| https://demo-store.example/products/linen-trousers | .product__description |
Rule reference: region
32. Dialog has no accessible name
Not a WCAG failure on its own (best practice) — 1 page, 1 instance — Page template · component: popup-newsletter
Why it matters: When a popup/modal opens, screen readers do not announce what it is. Affects screen-reader users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add aria-labelledby pointing at the dialog heading, or aria-label.
<div role="dialog" class="popup-newsletter"><button class="popup__close">×</button></div>
.popup-newsletter
https://demo-store.example/
<div role="dialog" class="popup-newsletter" aria-labelledby="dialog-title"> <!-- and id="dialog-title" on its heading -->
Generic before/after
<div role="dialog"> → <div role="dialog" aria-modal="true" aria-labelledby="popup-title"><h2 id="popup-title">Get 10% off</h2>
Rule reference: aria-dialog-name
33. Page has no main landmark
Not a WCAG failure on its own (best practice) — 2 pages, 2 instances — Single page
Why it matters: Screen-reader users cannot jump straight to the main content. Affects screen-reader users, keyboard-only users.
Fix once: this is one root cause repeated on 2 pages — fix the shared template, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Wrap the primary content in <main id="MainContent">.
<html>
html
https://demo-store.example/products/linen-shirt
<main id="MainContent" tabindex="-1"> … </main>
Generic before/after
<div id="content">…</div> → <main id="MainContent">…</main>
All affected locations (2)
| https://demo-store.example/products/linen-shirt | html |
| https://demo-store.example/products/linen-trousers | html |
Rule reference: landmark-one-main
34. Page has no main heading (h1)
Not a WCAG failure on its own (best practice) — 2 pages, 2 instances — Single page
Why it matters: Users jumping to the h1 cannot find the page topic. Affects screen-reader users.
Fix once: this is one root cause repeated on 2 pages — fix the shared template, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add exactly one h1 that describes the page (product title, collection name).
<html>
html
https://demo-store.example/products/linen-shirt
<h1 class="title">Summer Collection</h1>Platform pattern
<h1 class="title">{{ collection.title }}</h1> in sections/main-collection-banner.liquidAll affected locations (2)
| https://demo-store.example/products/linen-shirt | html |
| https://demo-store.example/products/linen-trousers | html |
Rule reference: page-has-heading-one
35. Content is outside landmarks
Not a WCAG failure on its own (best practice) — 1 page, 2 instances — Single page
Why it matters: Content not inside header/nav/main/footer can be missed when navigating by landmark. Affects screen-reader users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Place all visible content inside <header>, <nav>, <main>, <aside> or <footer>.
<h1>Your cart</h1>
h1
https://demo-store.example/cart
<header><div class="announcement">Free shipping</div></header>
All affected locations (2)
| https://demo-store.example/cart | h1 |
| https://demo-store.example/cart | table |
Rule reference: region
36. Page has no main landmark
Not a WCAG failure on its own (best practice) — 1 page, 1 instance — Single page
Why it matters: Screen-reader users cannot jump straight to the main content. Affects screen-reader users, keyboard-only users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Wrap the primary content in <main id="MainContent">.
<html>
html
https://demo-store.example/cart
<main id="MainContent" tabindex="-1"> … </main>
Generic before/after
<div id="content">…</div> → <main id="MainContent">…</main>
Rule reference: landmark-one-main
37. Content is outside landmarks
Not a WCAG failure on its own (best practice) — 1 page, 3 instances — Single page
Why it matters: Content not inside header/nav/main/footer can be missed when navigating by landmark. Affects screen-reader users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Place all visible content inside <header>, <nav>, <main>, <aside> or <footer>.
<h1>Contact</h1>
h1
https://demo-store.example/pages/contact
<header><div class="announcement">Free shipping</div></header>
All affected locations (3)
| https://demo-store.example/pages/contact | h1 |
| https://demo-store.example/pages/contact | input[name="contact[name]"] |
| https://demo-store.example/pages/contact | textarea |
Rule reference: region
38. Content is outside landmarks
Not a WCAG failure on its own (best practice) — 1 page, 3 instances — Single page
Why it matters: Content not inside header/nav/main/footer can be missed when navigating by landmark. Affects screen-reader users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Place all visible content inside <header>, <nav>, <main>, <aside> or <footer>.
<div class="banner__heading">Summer linen</div>
.banner__heading
https://demo-store.example/
<header><div class="announcement">Free shipping</div></header>
All affected locations (3)
| https://demo-store.example/ | .banner__heading |
| https://demo-store.example/ | img[src$="hero_2000x.jpg"] |
| https://demo-store.example/ | .product-grid |
Rule reference: region
39. Page has no main landmark
Not a WCAG failure on its own (best practice) — 1 page, 1 instance — Single page
Why it matters: Screen-reader users cannot jump straight to the main content. Affects screen-reader users, keyboard-only users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Wrap the primary content in <main id="MainContent">.
<html>
html
https://demo-store.example/pages/contact
<main id="MainContent" tabindex="-1"> … </main>
Generic before/after
<div id="content">…</div> → <main id="MainContent">…</main>
Rule reference: landmark-one-main
40. Page has no main landmark
Not a WCAG failure on its own (best practice) — 1 page, 1 instance — Single page
Why it matters: Screen-reader users cannot jump straight to the main content. Affects screen-reader users, keyboard-only users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Wrap the primary content in <main id="MainContent">.
<html>
html
https://demo-store.example/collections/all
<main id="MainContent" tabindex="-1"> … </main>
Generic before/after
<div id="content">…</div> → <main id="MainContent">…</main>
Rule reference: landmark-one-main
41. Content is outside landmarks
Not a WCAG failure on its own (best practice) — 1 page, 1 instance — Single page
Why it matters: Content not inside header/nav/main/footer can be missed when navigating by landmark. Affects screen-reader users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Place all visible content inside <header>, <nav>, <main>, <aside> or <footer>.
<div id="MainContent">
#MainContent
https://demo-store.example/collections/all
<header><div class="announcement">Free shipping</div></header>
Rule reference: region
Needs human review (2)
Automation could not decide these. They are not counted as failures.
1. Touch target is too small (WCAG 2.2)
2.5.8 Target Size (Minimum) — 6 pages, 12 instances — Theme / shared component · component: footer
Why it matters: Small, tightly packed controls are hard to hit for users with tremors or on mobile. Affects users with motor impairments.
Fix once: this is one root cause repeated on 6 pages — fix the shared theme component, not each page.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Make interactive targets at least 24×24 CSS px, or space them so a 24px circle around each does not overlap another.
<a href="https://instagram.com/demo" class="list-social__link"><svg class="icon icon-instagram" width="18" height="18"><rect width="18" height="18"></rect></svg></a>
li:nth-child(1) > .list-social__link[href$="demo"]
https://demo-store.example/
li:nth-child(1) > .list-social__link[href$="demo"] { min-width: 24px; min-height: 24px; }Generic before/after
.swatch { width: 16px; height: 16px; }
→
.swatch { min-width: 24px; min-height: 24px; }All affected locations (12)
| https://demo-store.example/ | li:nth-child(1) > .list-social__link[href$="demo"] |
| https://demo-store.example/ | li:nth-child(2) > .list-social__link[href$="demo"] |
| https://demo-store.example/cart | li:nth-child(1) > .list-social__link[href$="demo"] |
| https://demo-store.example/cart | li:nth-child(2) > .list-social__link[href$="demo"] |
| https://demo-store.example/products/linen-shirt | li:nth-child(1) > .list-social__link[href$="demo"] |
| https://demo-store.example/products/linen-shirt | li:nth-child(2) > .list-social__link[href$="demo"] |
| https://demo-store.example/pages/contact | li:nth-child(1) > .list-social__link[href$="demo"] |
| https://demo-store.example/pages/contact | li:nth-child(2) > .list-social__link[href$="demo"] |
| https://demo-store.example/collections/all | li:nth-child(1) > .list-social__link[href$="demo"] |
| https://demo-store.example/collections/all | li:nth-child(2) > .list-social__link[href$="demo"] |
| https://demo-store.example/products/linen-trousers | li:nth-child(1) > .list-social__link[href$="demo"] |
| https://demo-store.example/products/linen-trousers | li:nth-child(2) > .list-social__link[href$="demo"] |
Rule reference: target-size
2. No way to skip repeated navigation
2.4.1 Bypass Blocks — 1 page, 1 instance — Single page
Why it matters: Keyboard users must tab through the whole menu on every page. Affects keyboard-only users, screen-reader users.
Fix once: single location.
Where: Shopify admin → Online Store → Themes → … → Edit code. Header/footer/nav live in sections/header.liquid, sections/footer.liquid and snippets/*.liquid; product cards usually in snippets/card-product.liquid.
- Add a "Skip to content" link as the first focusable element, targeting <main id="MainContent">.
<html>
html
https://demo-store.example/
<a class="skip-link visually-hidden-focusable" href="#MainContent">Skip to content</a>
Generic before/after
<body><header>… → <body><a class="skip-link" href="#MainContent">Skip to content</a><header>…
Rule reference: bypass
Manual test checklist
Record PASS / FAIL / NOT_TESTED / NOT_APPLICABLE for each check.
| Check | WCAG | How to test | Result |
|---|---|---|---|
| Complete the main journey with the keyboard only | 2.1.1, 2.1.2, 2.4.3 |
| NOT_TESTED |
| Focus is always visible and not hidden behind sticky bars | 2.4.7, 2.4.11 |
| NOT_TESTED |
| Screen reader: headings and landmarks describe the page | 1.3.1, 2.4.6 |
| NOT_TESTED |
| Screen reader: every control announces name, role and state | 4.1.2, 2.5.3 |
| NOT_TESTED |
| Image alt text is meaningful (not just present) | 1.1.1 |
| NOT_TESTED |
| Form errors are announced and explained | 3.3.1, 3.3.3, 4.1.3 |
| NOT_TESTED |
| Popups and drawers manage focus correctly | 2.4.3, 2.1.2 |
| NOT_TESTED |
| Page works at 200% zoom and 320px width | 1.4.4, 1.4.10 |
| NOT_TESTED |
| Colour is not the only way information is shown | 1.4.1, 1.4.11 |
| NOT_TESTED |
| Checkout / payment widgets are usable | 2.1.1, 4.1.2 |
| NOT_TESTED |
| Confirm: Keyboard focus is not visible | 2.4.7 |
| NOT_TESTED |
| Confirm: Clickable element cannot be reached with the keyboard | 2.1.1 |
| NOT_TESTED |
| Confirm: Focus moves to hidden or off-screen content | 2.4.3 |
| NOT_TESTED |
| Confirm: Touch target is too small (WCAG 2.2) | 2.5.8 |
| NOT_TESTED |
| Confirm: No way to skip repeated navigation | 2.4.1 |
| NOT_TESTED |
Verification status & remediation history
Baseline scan. After you deploy fixes, run a re-scan: each root cause is marked VERIFIED when it no longer appears on the pages where it was found, or REGRESSION if a verified fix comes back.
Limitations
- Only the sampled pages and their current state were tested; pages sharing a template were represented by examples.
- Content behind log-ins, the hosted checkout and third-party iframes was not tested unless listed above.
- Keyboard results are heuristics from automated Tab traversal and must be confirmed by a person.
- Suggested code is derived from the rendered HTML (DOM), not your source repository; adapt it to your templates.
- No automated tool can guarantee legal compliance; this report does not replace a full manual audit or legal advice.