Accessibility
Accessibility ensures your theme works for all users, including those using assistive technologies, keyboard navigation, or experiencing temporary disabilities. Good accessibility also improves SEO and overall usability.
Semantic HTML
Section titled “Semantic HTML”Use Appropriate Elements
Section titled “Use Appropriate Elements”Semantic HTML provides meaning to assistive technologies:
{# Good: Semantic structure #}<article class="event"> <header> <h1>{{ event.title }}</h1> <time datetime="{{ event.startDate }}"> {{ event.startDate | date: '%B %d, %Y' }} </time> </header>
<main> {% stageblocks event %} </main>
<footer> <a href="/events">Back to Events</a> </footer></article>
{# Avoid: Divs for everything #}<div class="event"> <div class="event-header"> <div class="event-title">{{ event.title }}</div> <div class="event-date">{{ event.startDate | date: '%B %d, %Y' }}</div> </div></div>Heading Hierarchy
Section titled “Heading Hierarchy”Maintain a logical heading structure:
{# Good: Logical hierarchy #}<h1>{{ page.title }}</h1>
<section> <h2>Upcoming Events</h2> {% for event in events %} <article> <h3>{{ event.title }}</h3> </article> {% endfor %}</section>
<section> <h2>About Us</h2> <h3>Our Mission</h3> <h3>Our Team</h3></section>Landmarks
Section titled “Landmarks”Use landmark elements for page regions:
<body> <header role="banner"> {% render 'components/global-header' %} </header>
<nav aria-label="Main navigation"> {% render 'components/global-navigation' %} </nav>
<main id="main-content"> {{ content_for_layout }} </main>
<aside aria-label="Related content"> {% render 'components/sidebar' %} </aside>
<footer role="contentinfo"> {% render 'components/global-footer' %} </footer></body>Images and Media
Section titled “Images and Media”Meaningful Alt Text
Section titled “Meaningful Alt Text”Every image needs appropriate alt text:
{# Images that convey information need descriptive alt #}<img src="{{ person.image | image_url: width: 300 }}" alt="{{ person.name }}, {{ person.role }}">
<img src="{{ event.image | image_url: width: 800 }}" alt="{{ event.image.alt | default: event.title }}">{# Decorative images should have empty alt #}<img src="{{ 'decorative-border.svg' | asset_url }}" alt="" role="presentation">
{# Or use CSS background-image for purely decorative images #}{# Complex images (charts, diagrams) need extended descriptions #}<figure> <img src="{{ chart_image | image_url }}" alt="Season ticket sales by month" aria-describedby="chart-desc" > <figcaption id="chart-desc"> Bar chart showing sales increased 25% from January to March, with peak sales in February at 450 tickets. </figcaption></figure>Icon Accessibility
Section titled “Icon Accessibility”Icons need context for screen readers:
{# Icon with visible text - hide icon from AT #}<a href="/events"> <svg aria-hidden="true">...</svg> <span>View Events</span></a>
{# Icon-only button - needs accessible label #}<button type="button" aria-label="Close menu"> <svg aria-hidden="true"> <use xlink:href="#icon-close"></use> </svg></button>
{# Icon with visually hidden text #}<a href="/search"> <svg aria-hidden="true">...</svg> <span class="visually-hidden">Search</span></a>Visually Hidden Utility
Section titled “Visually Hidden Utility”Include a visually hidden class in your CSS:
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;}Keyboard Navigation
Section titled “Keyboard Navigation”Focus Management
Section titled “Focus Management”Ensure all interactive elements are keyboard accessible:
{# Good: Native button element #}<button type="button" onclick="toggleMenu()"> Menu</button>
{# Avoid: Div pretending to be a button #}<div class="button" onclick="toggleMenu()"> Menu</div>Focus Indicators
Section titled “Focus Indicators”Never remove focus outlines without providing alternatives:
/* Bad: Removes focus indication */*:focus { outline: none;}
/* Good: Custom focus indicator */*:focus { outline: 2px solid var(--focus-color); outline-offset: 2px;}
/* Good: Focus-visible for keyboard users only */*:focus:not(:focus-visible) { outline: none;}*:focus-visible { outline: 2px solid var(--focus-color); outline-offset: 2px;}Skip Links
Section titled “Skip Links”Provide a skip link to bypass navigation:
{# At the very top of the body #}<a href="#main-content" class="skip-link"> Skip to main content</a>
{# ... navigation ... #}
<main id="main-content"> {{ content_for_layout }}</main>.skip-link { position: absolute; top: -40px; left: 0; padding: 8px 16px; background: var(--brand-primary); color: white; z-index: 100;}
.skip-link:focus { top: 0;}Label Association
Section titled “Label Association”Every input needs an associated label:
{# Good: Explicit label association #}<label for="email">Email Address</label><input type="email" id="email" name="email" required>
{# Good: Implicit association #}<label> Email Address <input type="email" name="email" required></label>
{# Bad: No label association #}<span>Email Address</span><input type="email" name="email">Error Messages
Section titled “Error Messages”Make form errors accessible:
<div class="form-field {% if errors.email %}form-field--error{% endif %}"> <label for="email">Email Address</label> <input type="email" id="email" name="email" {% if errors.email %} aria-invalid="true" aria-describedby="email-error" {% endif %} required > {% if errors.email %} <p id="email-error" class="error-message" role="alert"> {{ errors.email }} </p> {% endif %}</div>Required Fields
Section titled “Required Fields”Indicate required fields clearly:
<label for="name"> Full Name <span class="required" aria-hidden="true">*</span> <span class="visually-hidden">(required)</span></label><input type="text" id="name" name="name" required aria-required="true">Color and Contrast
Section titled “Color and Contrast”Minimum Contrast Ratios
Section titled “Minimum Contrast Ratios”Follow WCAG contrast requirements:
| Content Type | Minimum Ratio |
|---|---|
| Normal text | 4.5:1 |
| Large text (18px+ or 14px+ bold) | 3:1 |
| UI components & graphics | 3:1 |
Don’t Rely on Color Alone
Section titled “Don’t Rely on Color Alone”Provide additional indicators beyond color:
{# Good: Color + icon + text #}<span class="status status--success"> <svg aria-hidden="true">...</svg> Available</span>
<span class="status status--error"> <svg aria-hidden="true">...</svg> Sold Out</span>
{# Bad: Color only #}<span class="status" style="color: green;">Available</span><span class="status" style="color: red;">Sold Out</span>Test with Tools
Section titled “Test with Tools”ARIA When Needed
Section titled “ARIA When Needed”Use ARIA Sparingly
Section titled “Use ARIA Sparingly”Native HTML is preferred over ARIA:
{# Good: Native HTML button #}<button type="button">Click me</button>
{# Unnecessary: ARIA on native element #}<button type="button" role="button">Click me</button>
{# When ARIA is needed: Custom components #}<div role="tablist" aria-label="Event details"> <button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" > Details </button> <button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" > Schedule </button></div>Common ARIA Patterns
Section titled “Common ARIA Patterns”Expandable Content
Section titled “Expandable Content”<button type="button" aria-expanded="false" aria-controls="accordion-content" onclick="toggleAccordion(this)"> Show More</button><div id="accordion-content" hidden> {{ content }}</div>Live Regions
Section titled “Live Regions”{# Announce dynamic content changes #}<div aria-live="polite" aria-atomic="true" class="visually-hidden"> {{ status_message }}</div>Modal Dialogs
Section titled “Modal Dialogs”<div role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-desc"> <h2 id="modal-title">Confirm Purchase</h2> <p id="modal-desc">Are you sure you want to purchase 2 tickets?</p> <button type="button">Confirm</button> <button type="button">Cancel</button></div>Testing Accessibility
Section titled “Testing Accessibility”Manual Testing
Section titled “Manual Testing”- Keyboard navigation: Tab through the page, can you reach everything?
- Screen reader: Use VoiceOver (Mac), NVDA (Windows), or JAWS
- Zoom: Does the page work at 200% zoom?
- Color: Can you understand the page in grayscale?
Automated Testing
Section titled “Automated Testing”- axe DevTools - Browser extension
- WAVE - wave.webaim.org
- Lighthouse - Accessibility audit in Chrome DevTools
Accessibility Checklist
Section titled “Accessibility Checklist”Structure
Section titled “Structure”- Page has one
<h1>that describes the page - Headings are in logical order (no skipped levels)
- Landmark regions are used (
<header>,<nav>,<main>,<footer>) - Skip link provided to bypass navigation
Images
Section titled “Images”- All images have appropriate
alttext - Decorative images have
alt="" - Complex images have extended descriptions
Keyboard
Section titled “Keyboard”- All interactive elements are keyboard accessible
- Focus indicator is visible
- Focus order is logical
- No keyboard traps
- All inputs have associated labels
- Required fields are indicated
- Error messages are associated with inputs
- Form errors are announced to screen readers
Color & Contrast
Section titled “Color & Contrast”- Text contrast meets 4.5:1 ratio
- Information isn’t conveyed by color alone
- Focus indicators have sufficient contrast
- ARIA only used when native HTML insufficient
- ARIA attributes are valid and complete
- Dynamic content uses
aria-liveregions