
liquid-theme-a11y
✓ Official★ 20by shopify · part of shopify/liquid-skills
Implement WCAG 2.2 accessibility patterns in Shopify Liquid themes. Covers e-commerce-specific components including product cards, carousels, cart drawers,…
This is the playbook your agent receives when the skill activates — you don't need to read it to use the skill, but it's here to audit before installing.
by shopify
Implement WCAG 2.2 accessibility patterns in Shopify Liquid themes. Covers e-commerce-specific components including product cards, carousels, cart drawers,…
npx skills add https://github.com/shopify/liquid-skills --skill liquid-theme-a11y
Download ZIPGitHub20
Accessibility for Shopify Liquid Themes
Core Principle
Every interactive component must work with keyboard only, screen readers, and reduced-motion preferences. Start with semantic HTML — add ARIA only when native semantics are insufficient.
Decision Table: Which Pattern?
Component HTML Element ARIA Pattern Reference
Expandable content <details>/<summary> None needed Accordion
Modal/dialog <dialog> aria-modal="true" Modal
Tooltip/popup [popover] attribute role="tooltip" fallback Tooltip
Dropdown menu <nav> + <ul> aria-expanded on triggers Navigation
Tab interface <div> role="tablist/tab/tabpanel" Tabs
Carousel/slider <div> role="region" + aria-roledescription Carousel
Product card <article> aria-labelledby Product card
Form <form> aria-invalid, aria-describedby Forms
Cart drawer <dialog> Focus trap Cart drawer
Price display <span> aria-label for context Prices
Filters <form> + <fieldset> aria-expanded for disclosures Filters
Page Structure
Landmarks
{{ 'accessibility.skip_to_content' | t }}
...
...
-
Single
<header>,<main>,<footer>per page -
Multiple
<nav>elements must have distinctaria-label -
All content must live inside a landmark
Skip Link
.skip-link {
position: absolute;
inset-inline-start: -999px;
z-index: 999;
}
.skip-link:focus {
position: fixed;
inset-block-start: 0;
inset-inline-start: 0;
padding: 1rem;
background: var(--color-background);
color: var(--color-foreground);
}
Headings
-
One
<h1>per page, never skip levels (h1 → h3) -
Use real heading elements, not styled divs
-
Template:
<h1>is typically the page/product title
Focus Management
Focus Indicators
/* All interactive elements */
:focus-visible {
outline: 2px solid rgb(var(--color-focus));
outline-offset: 2px;
}
/* High contrast mode */
@media (forced-colors: active) {
:focus-visible {
outline: 3px solid LinkText;
}
}
-
Minimum 3:1 contrast ratio for focus indicators
-
Use
:focus-visible(not:focus) to avoid showing on click -
Never
outline: nonewithout a visible replacement
Focus Trapping (Modals/Drawers)
-
Trap focus inside modals, drawers, and dialogs
-
Return focus to trigger element on close
-
First focusable element gets focus on open
-
Query all focusable elements:
a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])
See focus and keyboard patterns for full FocusTrap implementation.
Component Patterns
Product Card
###
{{ product.title }}
{{ product.price | money }}
{{ 'products.add_to_cart' | t }}
Rules:
-
Single tab stop per card (the main link)
-
tabindex="-1"on mouse-only shortcuts (quick add) -
aria-labelledbyon<article>pointing to the title -
Descriptive alt text on images; empty
alt=""if decorative
Carousel
{% render 'icon-chevron-left' %}
{% render 'icon-chevron-right' %}
{% render 'icon-pause' %}
{% for slide in section.blocks %}
{{ slide.settings.content }}
{% endfor %}
Rules:
-
Auto-rotation minimum 5 seconds, pause on hover/focus
-
Play/pause button required for auto-rotating carousels
-
aria-live="polite"on slide container (set to"off"during auto-rotation) -
aria-hidden="true"on inactive slides -
Each slide:
role="group"+aria-roledescription="slide"
Modal
## {{ title }}
{% render 'icon-close' %}
Rules:
-
Prefer native
<dialog>element for modal UI when feasible.showModal()provides native modal behavior, Escape-to-close, and backdrop handling, butrole="dialog"remains a valid fallback when native<dialog>is not a good fit. -
aria-labelledbypointing to the title (notaria-labelwith a string —aria-labelledbystays in sync when the title changes) -
Close on Escape key (native with
<dialog>) -
Focus first interactive element on open
-
Return focus to trigger on close
Cart Drawer
Same as modal pattern but with additional:
-
Live region for cart count updates:
<span aria-live="polite" aria-atomic="true"> -
Clear "remove item" buttons with
aria-label="{{ 'cart.remove_item' | t: title: item.title }}" -
Quantity inputs with associated labels
Forms
{{ 'forms.email' | t }}
{{ 'forms.email_required' | t }}
Rules:
-
Every input has a visible
<label>with matchingfor/id -
Use
<fieldset>/<legend>for radio/checkbox groups -
Error messages:
role="alert"+aria-describedbylinking to input -
aria-invalid="true"on invalid inputs -
autocompleteattributes on common fields -
Required fields:
required+aria-required="true"+ visual indicator
Product Filters
{{ 'filters.color' | t }}
{{ 'filters.filter_by_color' | t }}
{% for color in colors %}
{{ color }}
{% endfor %}
{{ 'filters.results_count' | t: count: results.size }}
Price Display
{% if product.compare_at_price > product.price %}
{{ product.compare_at_price | money }}
{{ product.price | money }}
{% else %}
{{ product.price | money }}
{% endif %}
-
Use
aria-labelon both sale and regular price paths — screen readers need context for any price display -
aria-hidden="true"on the visual strikethrough to avoid duplicate reading
Accordion
{{ block.settings.heading }}
{{ block.settings.content }}
Native <details>/<summary> provides keyboard and screen reader support automatically.
Tabs
{% for tab in tabs %}
{{ tab.title }}
{% endfor %}
{% for tab in tabs %}
{{ tab.content }}
{% endfor %}
-
Arrow keys navigate between tabs (left/right)
-
Only active tab has
tabindex="0", others-1
Dropdown Navigation
{% for link in linklists.main-menu.links %}
-
{% if link.links.size > 0 %}
{{ link.title }}
{% for child in link.links %}
- {{ child.title }}
{% endfor %}
{% else %}
{{ link.title }}
{% endif %}
{% endfor %}
Tooltip
{{ 'labels.info' | t }}
{{ block.settings.tooltip_text }}
Mobile Accessibility
-
Touch targets: minimum 44x44px, 8px spacing between targets
-
No orientation lock: never restrict to portrait/landscape
-
No hover-only content: everything accessible via tap
-
Use
dvhinstead ofvhfor mobile viewport units
Animation & Motion
/* Always provide reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
-
No flashing above 3 times per second
-
Auto-playing animations need pause/stop controls
-
Meaningful animations only — don't animate for decoration
Visually Hidden Utility
.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;
}
Use for screen-reader-only content like labels and descriptions.
Progressive Enhancement
Interactive components should work without JavaScript where possible. Provide <noscript> fallbacks for JS-dependent controls:
{%- comment -%} Variant picker with noscript fallback {%- endcomment -%}
{% for variant in product.variants %}
{{ variant.title }} - {{ variant.price | money }}
{% endfor %}
Live Region for Dynamic Updates
When selections change (variants, filters, cart), announce the change to screen readers:
{{ 'products.variant_selected' | t: variant: selected_variant.title }}
Use the clear-then-set pattern in JS to ensure announcements fire reliably:
announce(message) {
this.liveRegion.textContent = '';
requestAnimationFrame(() => {
this.liveRegion.textContent = message;
});
}
Color Contrast
Element Minimum Ratio Normal text (<18px / <14px bold) 4.5:1 Large text (≥18px / ≥14px bold) 3:1 UI components & graphics 3:1 Focus indicators 3:1
Never rely solely on color to convey information — always pair with text, icons, or patterns.
References
npx skills add https://github.com/shopify/liquid-skills --skill liquid-theme-a11yRun this in your project — your agent picks the skill up automatically.
No common issues documented yet. If you hit a problem, the repository's GitHub Issues page is the best place to look.