Free, open-source, WCAG-conscious Liquid components for Shopify themes — a modal, a cart drawer,
tabs, an accordion, a quantity selector, an image gallery, collapsible filters, and a skip link.
Every one is keyboard-operable, screen-reader labelled, honors prefers-reduced-motion, and
degrades to working markup if JavaScript fails to load.
→ Want to know how your live theme actually scores? Try the free audit — we built these components because we kept rebuilding the same accessible patterns from scratch for client themes, so we open-sourced them. No signup required.
Copy the snippet(s) you need into your theme's snippets/ folder, load the shared CSS/JS once,
and render them like any other snippet.
The tabs component (accessible-tabs.liquid), rendered:
<a11y-tabs class="a11y-tabs">
<div role="tablist" aria-label="Product information" class="a11y-tabs__list">
<button role="tab" aria-selected="true" tabindex="0" aria-controls="AccessiblePanel-product-info-0">Description</button>
<button role="tab" aria-selected="false" tabindex="-1" aria-controls="AccessiblePanel-product-info-1">Shipping</button>
<button role="tab" aria-selected="false" tabindex="-1" aria-controls="AccessiblePanel-product-info-2">Reviews</button>
</div>
<div role="tabpanel" id="AccessiblePanel-product-info-0" tabindex="0">…</div>
<div role="tabpanel" id="AccessiblePanel-product-info-1" tabindex="0" hidden>…</div>
<div role="tabpanel" id="AccessiblePanel-product-info-2" tabindex="0" hidden>…</div>
</a11y-tabs>Tab, Shift+Tab, and the Left/Right/Home/End arrow keys all work exactly as VoiceOver, NVDA, and JAWS users expect from the WAI-ARIA tabs pattern — no custom JS to write.
| Component | File | Pattern |
|---|---|---|
| Skip link | snippets/skip-to-content.liquid |
First focusable element on the page |
| Modal | snippets/accessible-modal.liquid |
Native <dialog>, focus trap, Escape + backdrop close |
| Cart drawer | snippets/accessible-cart-drawer.liquid |
Same as modal, plus a live region for cart changes |
| Accordion | snippets/accessible-accordion.liquid |
Native <details>/<summary>, no JS required |
| Tabs | snippets/accessible-tabs.liquid |
WAI-ARIA tabs, roving tabindex, arrow-key navigation |
| Quantity selector | snippets/accessible-quantity-selector.liquid |
Labelled stepper, live region announces the new value |
| Image gallery | snippets/accessible-image-gallery.liquid |
Keyboard-navigable thumbnails, live region on selection |
| Product filters | snippets/accessible-product-filters.liquid |
Disclosure groups, live region announces result count |
Shared behavior lives in two files so it's loaded once, not duplicated per component:
assets/accessible-components.css— visually-hidden utility, skip-link, focus-visible rings,prefers-reduced-motionhandling, and each component's layout stylesassets/accessible-components.js— aFocusTrapclass, a clear-then-setannounce()helper for live regions, and one Web Component per interactive pattern (<a11y-modal>,<a11y-cart-drawer>,<a11y-tabs>,<a11y-quantity-selector>,<a11y-gallery>,<a11y-filters>)
-
Copy the snippet(s) you want from
snippets/into your theme'ssnippets/folder. -
Copy
assets/accessible-components.cssandassets/accessible-components.jsinto your theme'sassets/folder. -
Copy the keys you need from
locales/en.default.jsoninto your theme's own locale file (or merge the whole file in if you don't have conflicting keys). -
Load the shared CSS and JS once, in
layout/theme.liquid:{{ 'accessible-components.css' | asset_url | stylesheet_tag }} <script src="{{ 'accessible-components.js' | asset_url }}" defer="defer"></script>
{% render 'accessible-modal', id: 'size-guide', trigger_label: 'Size guide', title: 'Size guide', content: section.settings.size_guide_content %}
{% render 'accessible-tabs', id: 'product-info', label: 'Product information', tabs: product.metafields.custom.info_tabs.value %}
{% render 'accessible-accordion', items: product.metafields.custom.faqs.value %}
{% render 'accessible-quantity-selector', id: product.id, value: 1, min: 1 %}
{% render 'accessible-image-gallery', images: product.images, alt_fallback: product.title %}
{% render 'accessible-product-filters', filters: collection.filters, result_count: collection.products_count %}
{% render 'accessible-cart-drawer' %}
{% render 'skip-to-content' %}Every snippet has a {% doc %} header with the full parameter list and a copy-pasteable
@example — see the file itself, or examples/product-page-example.liquid for a full
composition on a product page.
- Not a full accessible theme. These are components to drop into your existing theme, not a theme-check replacement or an automatic fix for every accessibility issue on your store — contrast, heading structure, and content-level issues (missing alt text on merchant-uploaded images, for example) are still yours to fix.
- Not an automated scanner. It doesn't test your live store; it gives you correct markup and behavior for eight common patterns. For an automated pass at what's actually on your store, see the audit link above or shopify-store-audit-toolkit.
- No build step, but no framework integration either. These are Web Components on native browser APIs — if your theme already uses a JS framework, you may want to wrap them rather than use them as-is.
- Not a full design system. The CSS provides functional, accessible defaults (spacing, focus rings, touch targets) — expect to reskin colors and typography to match your brand.
Does this need a build step? No. No npm install, no bundler — copy the files, load two
<script>/<link> tags, done.
Will this conflict with my theme's existing modal/tabs/etc.? The custom elements
(<a11y-modal> and friends) and CSS classes (.a11y-*) are namespaced, so they shouldn't collide
with an existing implementation. Use one or the other for a given component rather than both.
Does the cart drawer talk to Shopify's cart API for me? No — it renders cart.items from
Liquid on page load and exposes .open(), .close(), and .announceCountChange(count) so your
existing /cart/add.js / /cart/change.js fetch logic can drive it. It doesn't assume a
particular cart-update implementation.
Why native <dialog> instead of a custom overlay div? Focus management, Escape-to-close, and
top-layer stacking come from the browser for free and are more reliable than reimplementing them.
Every evergreen browser supports it.
Also from Ecom Swift LLC, free and open source:
- shopify-store-audit-toolkit — CLI that checks accessibility basics (and SEO, performance, structured data) against a live store
- shopify-theme-performance-auditor — audits theme source files for large assets and render-blocking resources
- shopify-seo-checklist — the technical and on-page SEO pass these components don't cover
- Full list: github.com/EcomswiftLLC · github.com/Ecom-Swift-LLC
- More free tools: www.ecomswiftllc.com/free-tools
Issues and pull requests are welcome — another common pattern (a select/combobox, a rating widget), a bug in the keyboard handling, or a WCAG detail we got wrong. Please include:
- Which component and Shopify theme/browser combination
- Expected vs. actual keyboard/screen-reader behavior
- A minimal reproduction where possible
- Accessible select/combobox for variant pickers
- Accessible star-rating display
- Automated axe-core checks in CI against the
examples/composition
MIT © Ecom Swift LLC — see LICENSE.
This project is maintained by Ecom Swift LLC, a Shopify Partner.
- 🛍️ Shopify Partner Directory: https://www.shopify.com/partners/directory/partner/waowy
- ✉️ Email: support@ecomswiftllc.com
- 💬 WhatsApp: https://wa.me/16312511767
These eight components fix common patterns in your theme code. The free audit at audit.ecomswiftllc.com checks your actual live store — accessibility basics, SEO, speed, and AI/LLM visibility (can ChatGPT, Perplexity and Google AI actually see and cite your store) — in about a minute, no signup.
More free tools: www.ecomswiftllc.com/free-tools · Website: www.ecomswiftllc.com · Partner Directory: shopify.com/partners/directory/partner/waowy