Accessibility
An accessible theme can be used by everyone, including people who rely on keyboards, screen readers, or other assistive technology. Aim to meet WCAG 2.1 level AA, and follow these practices as you build.
Use semantic HTML and a logical heading order
Use the right element for the job — <button> for actions, <a> for links, <nav>, <main>, and <header> for landmarks. Native elements come with built-in keyboard and screen-reader support that custom <div> widgets don't.
Give each page a single <h1> and don't skip heading levels (<h1> → <h2> → <h3>). Headings should describe the page structure, not be chosen for their size.
For example, wrap the page in landmark elements and reach for <button>/<a> instead of a click-handled <div>:
<header>...</header>
<nav aria-label="Primary">...</nav>
<main id="main-content">
<h1>Product title</h1>
<button type="button">Add to cart</button>
</main>
<footer>...</footer>
Make everything keyboard accessible
Every interactive element must be reachable and operable with the keyboard alone. Verify that:
- The focus order matches the visual and DOM order (top to bottom, left to right).
- Focused elements show a visible focus indicator — don't remove outlines without providing a replacement.
- Dropdowns, drawers, and modals can be opened, operated, and closed (including with the
Esckey) using the keyboard.
If you restyle the focus indicator, keep it clearly visible rather than removing it:
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
Provide a skip link
Keyboard and screen-reader users start at the top of the page and tab through everything in order. Without help, they have to step through the entire header and navigation on every page before reaching the content. Add a "skip to content" link as the first focusable element so they can jump straight to the main content:
<a href="#main-content" class="skip-link">Skip to content</a>
Point it at the id of your main content element, and reveal the link only when it receives focus:
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus {
left: 0;
}
Provide text alternatives for images
Every <img> needs an alt attribute. For content images, output the image's alt text from the image object:
<img src="{{ product.image | img_url: '600x' }}" alt="{{ product.image.alt | escape }}">
For decorative images that add no information, use an empty alt="" so screen readers skip them.
Ensure sufficient color contrast
Maintain a contrast ratio of at least 4.5:1 between text and its background. For large text (24px, or 18.66px bold) and for meaningful non-text elements such as icons and input borders, the minimum is 3:1. Don't rely on color alone to convey information — pair it with text or an icon.
To achieve this in practice:
- Measure each pair: Check the foreground and background colors with a contrast checker — your browser's DevTools shows the ratio in the color picker, or use the WebAIM Contrast Checker. For example,
#767676on white is 4.54:1 (passes for body text), while#999999on white is only 2.85:1 (fails). - Adjust until it passes: If a pair falls short, darken the text or lighten the background until it clears the threshold — small lightness changes are usually enough.
- Watch text over images and gradients: Contrast varies across the image, so add a solid or semi-transparent overlay (a scrim) behind the text.
- Don't forget settings: Merchants often recolor a theme; test the color combinations your theme settings allow, not just the defaults.
Label every form input
Associate a <label> with each input using matching for and id attributes, so assistive technology announces the field's purpose:
<label for="contact-email">Email</label>
<input id="contact-email" type="email" name="email">
When a field has an error, link the message to the input with aria-describedby, and don't communicate the error with color alone.
Make dynamic components accessible
For interactive components, expose their state and purpose with ARIA:
- On a toggle such as a menu or accordion, set
aria-expandedand pointaria-controlsat the element it opens. - Give modals and drawers
role="dialog"andaria-modal="true", move focus into them when they open, trap focus while open, and return focus to the trigger when they close.
For example, a menu toggle reflects its open state in aria-expanded (your JavaScript flips it to true when the drawer opens):
<button aria-expanded="false" aria-controls="menu-drawer">Menu</button>
<div id="menu-drawer" hidden>...</div>
Keep labels translatable by sourcing them from locale files with the t filter, for example aria-label="{{ 'i18n.general.search' | t }}". See Internationalization.
Size touch targets
Make touch targets at least 24 by 24 CSS pixels so they're easy to tap, with adequate spacing between adjacent targets:
.nav-icon {
min-width: 24px;
min-height: 24px;
}
Respect reduced motion
Large animations, parallax effects, and auto-playing carousels can cause discomfort or nausea for users with vestibular disorders. Operating systems expose a "reduce motion" setting; honor it with the prefers-reduced-motion media query, which turns animation off for those users while keeping it for everyone else:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Test your theme
Check accessibility throughout development:
- Navigate the whole page using only the keyboard.
- Run a screen reader (such as VoiceOver or NVDA) over key flows like browsing and checkout.
- Verify color contrast with a contrast checker.
- Validate your HTML to catch structural issues.