10 Most Common WCAG Violations in Shopify Stores (And How to Fix Them)
A practical, code-focused guide to identifying and fixing the accessibility issues we see most often in Shopify stores. Includes working code examples and Liquid snippets you can implement today.
After scanning thousands of Shopify stores, we've identified the most common accessibility violations that prevent sites from meeting WCAG 2.1 AA and EAA compliance. The good news? Most of these issues are straightforward to fix with the right code.
This guide provides working code examples for each issue, including HTML, CSS, and Shopify Liquid templates. You can copy these solutions directly into your theme.
Jump to a Violation
Missing Alt Text on Product Images
✕Problem Code
<!-- Bad: No alt text -->
<img src="product-blue-shirt.jpg">
<!-- Also bad: Non-descriptive alt -->
<img src="product-blue-shirt.jpg" alt="image">
<img src="product-blue-shirt.jpg" alt="product">✓Accessible Solution
<!-- Good: Descriptive alt text -->
<img
src="product-blue-shirt.jpg"
alt="Men's Navy Blue Oxford Button-Down Shirt, Size M"
>
<!-- For decorative images -->
<img src="decorative-pattern.jpg" alt="" role="presentation">⚡Shopify Liquid Implementation
{% comment %} In your product template {% endcomment %}
<img
src="{{ product.featured_image | img_url: 'large' }}"
alt="{{ product.featured_image.alt | default: product.title }}"
loading="lazy"
>Every meaningful image needs alt text that describes its content and function. For products, include key details like color, style, and type.
Insufficient Color Contrast
✕Problem Code
/* Bad: Light gray on white background */
.product-price {
color: #999999; /* Contrast ratio: 2.8:1 */
background: #ffffff;
}
/* Bad: Trendy but inaccessible */
.sale-badge {
color: #ff6b6b; /* Contrast ratio: 3.2:1 */
background: #fff5f5;
}✓Accessible Solution
/* Good: Meets 4.5:1 minimum ratio */
.product-price {
color: #595959; /* Contrast ratio: 7:1 */
background: #ffffff;
}
/* Good: Bold colors that work */
.sale-badge {
color: #c92a2a; /* Contrast ratio: 6.5:1 */
background: #fff5f5;
}⚡Shopify Liquid Implementation
{% comment %} In theme.scss or theme.css {% endcomment %}
:root {
--color-text-primary: #1a1a1a; /* 16:1 on white */
--color-text-secondary: #4a4a4a; /* 9:1 on white */
--color-text-muted: #666666; /* 5.7:1 on white */
--color-accent: #0052cc; /* 7.3:1 on white */
}Text must have a contrast ratio of at least 4.5:1 against its background (3:1 for large text). Use tools like WebAIM Contrast Checker.
Missing Form Labels
✕Problem Code
<!-- Bad: No label association -->
<input type="email" placeholder="Enter your email">
<!-- Bad: Label exists but not associated -->
<label>Email</label>
<input type="email" id="email-field">
<!-- Bad: Hidden label breaks accessibility -->
<label style="display: none;">Email</label>
<input type="email">✓Accessible Solution
<!-- Good: Properly associated label -->
<label for="email-field">Email Address</label>
<input type="email" id="email-field" required>
<!-- Good: Visually hidden but accessible -->
<label for="search" class="visually-hidden">Search products</label>
<input type="search" id="search" placeholder="Search...">
<!-- CSS for visually-hidden -->
<style>
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
</style>⚡Shopify Liquid Implementation
{% comment %} Newsletter signup form {% endcomment %}
<form action="/contact#contact_form" method="post">
<div class="form-group">
<label for="newsletter-email">
Email Address
<span class="required" aria-hidden="true">*</span>
</label>
<input
type="email"
id="newsletter-email"
name="contact[email]"
required
aria-required="true"
>
</div>
<button type="submit">Subscribe</button>
</form>Every form input needs a programmatically associated label using the for/id relationship. Placeholders are not labels.
Non-Descriptive Link Text
✕Problem Code
<!-- Bad: Non-descriptive links -->
<a href="/products/shirt-123">Click here</a>
<a href="/products/shirt-123">Read more</a>
<a href="/products/shirt-123">Learn more</a>
<!-- Bad: URL as link text -->
<a href="/shipping">www.store.com/shipping</a>
<!-- Bad: Image link without alt -->
<a href="/cart"><img src="cart-icon.svg"></a>✓Accessible Solution
<!-- Good: Descriptive link text -->
<a href="/products/shirt-123">
View Navy Blue Oxford Shirt Details
</a>
<!-- Good: Link with context -->
<a href="/shipping">
View our shipping policy and delivery times
</a>
<!-- Good: Image link with alt -->
<a href="/cart" aria-label="Shopping cart, 3 items">
<img src="cart-icon.svg" alt="">
<span class="cart-count">3</span>
</a>⚡Shopify Liquid Implementation
{% comment %} Product card link {% endcomment %}
<a
href="{{ product.url }}"
class="product-card-link"
aria-label="View {{ product.title }} - {{ product.price | money }}"
>
<img
src="{{ product.featured_image | img_url: 'medium' }}"
alt=""
>
<h3>{{ product.title }}</h3>
<span class="price">{{ product.price | money }}</span>
</a>Link text should describe the destination or action. Avoid generic phrases like "click here" or "read more".
Missing Skip Navigation Links
✕Problem Code
<!-- Bad: No skip link -->
<header>
<nav>
<!-- 50+ navigation links -->
</nav>
</header>
<main>
<!-- Content -->
</main>✓Accessible Solution
<!-- Good: Skip link as first focusable element -->
<body>
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<header>
<nav aria-label="Main navigation">
<!-- Navigation links -->
</nav>
</header>
<main id="main-content" tabindex="-1">
<!-- Content -->
</main>
</body>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px 16px;
background: #000;
color: #fff;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
</style>⚡Shopify Liquid Implementation
{% comment %} In theme.liquid, right after <body> {% endcomment %}
<a class="skip-to-content-link" href="#MainContent">
{{ 'accessibility.skip_to_content' | t }}
</a>
{% comment %} In your main content area {% endcomment %}
<main id="MainContent" class="content-for-layout" role="main" tabindex="-1">
{{ content_for_layout }}
</main>Skip links allow keyboard users to bypass repetitive content like navigation menus.
Keyboard Navigation Issues
✕Problem Code
<!-- Bad: Click-only interaction -->
<div onclick="addToCart()">Add to Cart</div>
<!-- Bad: Removes keyboard focus -->
<button onclick="..." style="outline: none;">Buy Now</button>
<!-- Bad: Non-focusable interactive element -->
<span class="dropdown-trigger">Select Size</span>✓Accessible Solution
<!-- Good: Proper button element -->
<button type="button" onclick="addToCart()">
Add to Cart
</button>
<!-- Good: Custom focus style instead of removing -->
<button type="button" class="buy-button">Buy Now</button>
<style>
.buy-button:focus {
outline: 2px solid #0052cc;
outline-offset: 2px;
}
</style>
<!-- Good: Keyboard accessible custom dropdown -->
<button
type="button"
aria-expanded="false"
aria-haspopup="listbox"
aria-controls="size-options"
>
Select Size
</button>⚡Shopify Liquid Implementation
{% comment %} Accessible add to cart button {% endcomment %}
<button
type="submit"
name="add"
class="btn product-form__cart-submit"
{% if product.selected_or_first_available_variant.available == false %}
disabled
aria-disabled="true"
{% endif %}
>
<span>
{% if product.selected_or_first_available_variant.available %}
{{ 'products.product.add_to_cart' | t }}
{% else %}
{{ 'products.product.sold_out' | t }}
{% endif %}
</span>
</button>All interactive elements must be keyboard accessible. Use semantic HTML elements and never remove focus indicators.
Missing Language Attribute
✕Problem Code
<!-- Bad: No language declared -->
<!DOCTYPE html>
<html>
<head>...</head>
<!-- Bad: Wrong language code -->
<html lang="english">✓Accessible Solution
<!-- Good: Correct language attribute -->
<!DOCTYPE html>
<html lang="en">
<!-- For German -->
<html lang="de">
<!-- For French -->
<html lang="fr">
<!-- For content in different language -->
<p>The French word for hello is
<span lang="fr">bonjour</span>.
</p>⚡Shopify Liquid Implementation
{% comment %} In theme.liquid {% endcomment %}
<!doctype html>
<html lang="{{ request.locale.iso_code }}">
<head>
<!-- head content -->
</head>
{% comment %} For multi-language content {% endcomment %}
{% if request.locale.iso_code == 'de' %}
<p lang="en">English description here</p>
{% endif %}The lang attribute helps screen readers use correct pronunciation and enables proper text-to-speech.
Improper Heading Hierarchy
✕Problem Code
<!-- Bad: Skipping heading levels -->
<h1>Our Store</h1>
<h4>Featured Products</h4> <!-- Skipped h2 and h3 -->
<!-- Bad: Multiple h1 elements -->
<h1>Store Name</h1>
<h1>Sale Banner</h1>
<h1>Product Title</h1>
<!-- Bad: Styled text instead of headings -->
<div class="big-text">Product Categories</div>✓Accessible Solution
<!-- Good: Logical heading structure -->
<h1>Blue Oxford Shirt</h1>
<h2>Product Details</h2>
<h3>Materials & Care</h3>
<h3>Size Guide</h3>
<h2>Customer Reviews</h2>
<h3>5 Star Reviews (42)</h3>
<h3>4 Star Reviews (18)</h3>
<h2>Related Products</h2>⚡Shopify Liquid Implementation
{% comment %} Product page structure {% endcomment %}
<main id="MainContent">
<h1 class="product-title">{{ product.title }}</h1>
<section aria-labelledby="description-heading">
<h2 id="description-heading">Description</h2>
{{ product.description }}
</section>
<section aria-labelledby="reviews-heading">
<h2 id="reviews-heading">Customer Reviews</h2>
<!-- Review content -->
</section>
</main>Headings should follow a logical hierarchy (h1, h2, h3...) without skipping levels. Only one h1 per page.
Missing Focus Indicators
✕Problem Code
/* Bad: Removing focus outlines */
*:focus {
outline: none;
}
button:focus {
outline: 0;
}
/* Bad: Focus style too subtle */
a:focus {
outline: 1px dotted #ccc;
}✓Accessible Solution
/* Good: Clear, visible focus indicators */
:focus {
outline: 2px solid #0052cc;
outline-offset: 2px;
}
/* Good: Custom focus styles that match brand */
:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(0, 82, 204, 0.5);
}
/* Good: Different styles for different elements */
button:focus-visible {
outline: 2px solid #0052cc;
outline-offset: 2px;
}
a:focus-visible {
background-color: #fff3cd;
outline: 2px solid #0052cc;
}⚡Shopify Liquid Implementation
{% comment %} In theme.scss or theme.css {% endcomment %}
/* Focus styles for all interactive elements */
:focus-visible {
outline: 3px solid var(--color-focus, #0052cc);
outline-offset: 2px;
}
/* Remove default outline only when using focus-visible */
:focus:not(:focus-visible) {
outline: none;
}
/* Ensure buttons have visible focus */
.btn:focus-visible,
.product-form__cart-submit:focus-visible {
outline: 3px solid var(--color-focus, #0052cc);
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(0, 82, 204, 0.2);
}Focus indicators are essential for keyboard navigation. Never remove them without providing a visible alternative.
Inaccessible Dropdown Menus
✕Problem Code
<!-- Bad: Hover-only dropdown -->
<div class="dropdown">
<span>Categories</span>
<ul class="dropdown-menu">
<li><a href="#">Shirts</a></li>
<li><a href="#">Pants</a></li>
</ul>
</div>
<style>
.dropdown-menu { display: none; }
.dropdown:hover .dropdown-menu { display: block; }
</style>✓Accessible Solution
<!-- Good: Keyboard accessible dropdown -->
<div class="dropdown">
<button
type="button"
aria-expanded="false"
aria-haspopup="true"
aria-controls="dropdown-menu"
>
Categories
<span aria-hidden="true">▼</span>
</button>
<ul
id="dropdown-menu"
role="menu"
aria-label="Categories"
hidden
>
<li role="none">
<a href="#" role="menuitem">Shirts</a>
</li>
<li role="none">
<a href="#" role="menuitem">Pants</a>
</li>
</ul>
</div>
<script>
// Toggle on click and keyboard
// Handle Escape to close
// Handle arrow key navigation
</script>⚡Shopify Liquid Implementation
{% comment %} Accessible navigation menu {% endcomment %}
<nav aria-label="Main navigation">
<ul class="nav-menu" role="menubar">
{% for link in linklists.main-menu.links %}
{% if link.links.size > 0 %}
<li role="none">
<button
type="button"
aria-expanded="false"
aria-haspopup="true"
aria-controls="submenu-{{ forloop.index }}"
class="nav-link has-dropdown"
>
{{ link.title }}
</button>
<ul
id="submenu-{{ forloop.index }}"
role="menu"
class="dropdown-menu"
hidden
>
{% for sublink in link.links %}
<li role="none">
<a href="{{ sublink.url }}" role="menuitem">
{{ sublink.title }}
</a>
</li>
{% endfor %}
</ul>
</li>
{% else %}
<li role="none">
<a href="{{ link.url }}" role="menuitem">
{{ link.title }}
</a>
</li>
{% endif %}
{% endfor %}
</ul>
</nav>Dropdown menus must work with keyboard alone. Use proper ARIA attributes and handle keyboard events.
How to Test Your Fixes
2. Keyboard Testing
Tab through your entire site using only the keyboard. Can you access everything? Can you see where focus is? Can you escape from modal dialogs?
3. Screen Reader Testing
Use VoiceOver (Mac), NVDA (Windows), or TalkBack (Android) to navigate your site. Do images have meaningful descriptions? Are forms labeled correctly?
4. Contrast Checker
Use WebAIM's Contrast Checker or the Chrome DevTools accessibility panel to verify your color combinations meet the 4.5:1 ratio requirement.
Scan Your Shopify Store Now
Find all these issues and more with our free accessibility scanner. Get a detailed report with line-by-line code fixes.
Free Accessibility Scan