
Liquid Theme A11y
- 2.2k installs
- 115 repo stars
- Updated July 16, 2026
- benjaminsehl/liquid-skills
liquid-theme-a11y is an agent skill that Implement WCAG 2.2 accessibility patterns in Shopify Liquid themes. Covers e-commerce-specific components including prod.
About
Accessibility for Shopify Liquid Themes 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 Component HTML Element ARIA Pattern Reference Expandable content details summary None needed Accordion accordion Modal dialog dialog aria modal true Modal modal Tooltip popup popover attribute role tooltip fallback Tooltip tooltip Dropdown menu nav ul aria expanded on triggers Navigation dropdown navigation Tab interface div role tablist tab tabpanel Tabs tabs Carousel slider div role region aria roledescription Carousel carousel Product card article aria labelledby Product card product card Form form aria invalid aria describedby Forms forms Cart drawer dialog Focus trap Cart drawer cart drawer Price display span aria label for context Prices price display Filters form fieldset aria expanded for disclosures Filters product filters html body a href main content class skip link accessibility skip_to_content t a header role banner nav aria label accessibility main_navigation t nav header main id main content All page content inside main
- description: "Implement WCAG 2.2 accessibility patterns in Shopify Liquid themes. Covers e-commerce-specific components
- Every interactive component must work with keyboard only, screen readers, and reduced-motion preferences. Start with sem
- | Component | HTML Element | ARIA Pattern | Reference |
- Follow liquid-theme-a11y SKILL.md steps and documented constraints.
- Follow liquid-theme-a11y SKILL.md steps and documented constraints.
Liquid Theme A11y by the numbers
- 2,218 all-time installs (skills.sh)
- +65 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #512 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
liquid-theme-a11y capabilities & compatibility
- Capabilities
- description: "implement wcag 2.2 accessibility p · every interactive component must work with keybo · | component | html element | aria pattern | refe · follow liquid theme a11y skill.md steps and docu
- Use cases
- orchestration
What liquid-theme-a11y says it does
description: "Implement WCAG 2.2 accessibility patterns in Shopify Liquid themes. Covers e-commerce-specific components including product cards, carousels, cart drawers, price display, forms, filters,
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.
| Component | HTML Element | ARIA Pattern | Reference |
npx skills add https://github.com/benjaminsehl/liquid-skills --skill liquid-theme-a11yAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.2k |
|---|---|
| repo stars | ★ 115 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 16, 2026 |
| Repository | benjaminsehl/liquid-skills ↗ |
When should an agent use liquid-theme-a11y and what problem does it solve?
Implement WCAG 2.2 accessibility patterns in Shopify Liquid themes. Covers e-commerce-specific components including product cards, carousels, cart drawers, price display, forms, filters, and modals. U
Who is it for?
Developers invoking liquid-theme-a11y as documented in the skill source.
Skip if: Skip when requirements fall outside liquid-theme-a11y documented scope.
When should I use this skill?
Implement WCAG 2.2 accessibility patterns in Shopify Liquid themes. Covers e-commerce-specific components including product cards, carousels, cart drawers, price display, forms, filters, and modals. U
What you get
Outputs aligned with the liquid-theme-a11y SKILL.md workflow and stated deliverables.
- Accessible Liquid HTML snippets
- ARIA component patterns
Files
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
<body>
<a href="#main-content" class="skip-link">{{ 'accessibility.skip_to_content' | t }}</a>
<header role="banner">
<nav aria-label="{{ 'accessibility.main_navigation' | t }}">...</nav>
</header>
<main id="main-content">
<!-- All page content inside main -->
</main>
<footer role="contentinfo">
<nav aria-label="{{ 'accessibility.footer_navigation' | t }}">...</nav>
</footer>
</body>- 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
<article class="product-card" aria-labelledby="ProductTitle-{{ product.id }}">
<a href="{{ product.url }}" class="product-card__link" aria-labelledby="ProductTitle-{{ product.id }}">
<img
src="{{ product.featured_image | image_url: width: 400 }}"
alt="{{ product.featured_image.alt | escape }}"
loading="lazy"
width="{{ product.featured_image.width }}"
height="{{ product.featured_image.height }}"
>
</a>
<h3 id="ProductTitle-{{ product.id }}">
<a href="{{ product.url }}">{{ product.title }}</a>
</h3>
<div class="product-card__price" aria-label="{{ 'products.price_label' | t: price: product.price | money }}">
{{ product.price | money }}
</div>
<button
class="product-card__quick-add"
tabindex="-1"
aria-label="{{ 'products.quick_add' | t: title: product.title }}"
>
{{ 'products.add_to_cart' | t }}
</button>
</article>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
<div
role="region"
aria-roledescription="carousel"
aria-label="{{ section.settings.heading | escape }}"
>
<div class="carousel__controls">
<button
aria-label="{{ 'accessibility.previous_slide' | t }}"
aria-controls="CarouselSlides-{{ section.id }}"
>{% render 'icon-chevron-left' %}</button>
<button
aria-label="{{ 'accessibility.next_slide' | t }}"
aria-controls="CarouselSlides-{{ section.id }}"
>{% render 'icon-chevron-right' %}</button>
<button
aria-label="{{ 'accessibility.pause_slideshow' | t }}"
aria-pressed="false"
>{% render 'icon-pause' %}</button>
</div>
<div id="CarouselSlides-{{ section.id }}" aria-live="polite">
{% for slide in section.blocks %}
<div
role="group"
aria-roledescription="slide"
aria-label="{{ 'accessibility.slide_n_of_total' | t: n: forloop.index, total: forloop.length }}"
{% unless forloop.first %}aria-hidden="true"{% endunless %}
>
{{ slide.settings.content }}
</div>
{% endfor %}
</div>
</div>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
<dialog
id="Modal-{{ section.id }}"
aria-labelledby="ModalTitle-{{ section.id }}"
aria-modal="true"
>
<div class="modal__header">
<h2 id="ModalTitle-{{ section.id }}">{{ title }}</h2>
<button
type="button"
aria-label="{{ 'accessibility.close' | t }}"
on:click="/closeModal"
>{% render 'icon-close' %}</button>
</div>
<div class="modal__content">
<!-- Content -->
</div>
</dialog>Rules:
- Use native
<dialog>element aria-labelledbypointing to the title- 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
<form action="{{ routes.cart_url }}" method="post">
<div class="form__field">
<label for="Email-{{ section.id }}">{{ 'forms.email' | t }}</label>
<input
type="email"
id="Email-{{ section.id }}"
name="email"
required
aria-required="true"
autocomplete="email"
aria-describedby="EmailError-{{ section.id }}"
>
<p
id="EmailError-{{ section.id }}"
class="form__error"
role="alert"
hidden
>{{ 'forms.email_required' | t }}</p>
</div>
</form>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 inputsautocompleteattributes on common fields- Required fields:
required+aria-required="true"+ visual indicator
Product Filters
<form class="facets">
<div class="facets__group">
<button
type="button"
aria-expanded="false"
aria-controls="FilterColor-{{ section.id }}"
>{{ 'filters.color' | t }}</button>
<fieldset id="FilterColor-{{ section.id }}" hidden>
<legend class="visually-hidden">{{ 'filters.filter_by_color' | t }}</legend>
{% for color in colors %}
<label>
<input type="checkbox" name="filter.color" value="{{ color }}">
{{ color }}
</label>
{% endfor %}
</fieldset>
</div>
<div aria-live="polite" aria-atomic="true">
{{ 'filters.results_count' | t: count: results.size }}
</div>
</form>Price Display
{% if product.compare_at_price > product.price %}
<div class="price" aria-label="{{ 'products.sale_price_label' | t: sale_price: product.price | money, original_price: product.compare_at_price | money }}">
<s aria-hidden="true">{{ product.compare_at_price | money }}</s>
<span>{{ product.price | money }}</span>
</div>
{% else %}
<div class="price">{{ product.price | money }}</div>
{% endif %}- Use
aria-labelto provide full price context (sale vs. original) aria-hidden="true"on the visual strikethrough to avoid duplicate reading
Accordion
<details>
<summary>{{ block.settings.heading }}</summary>
<div class="accordion__content">
{{ block.settings.content }}
</div>
</details>Native <details>/<summary> provides keyboard and screen reader support automatically.
Tabs
<div role="tablist" aria-label="{{ 'accessibility.product_tabs' | t }}">
{% for tab in tabs %}
<button
role="tab"
id="Tab-{{ tab.id }}"
aria-selected="{% if forloop.first %}true{% else %}false{% endif %}"
aria-controls="Panel-{{ tab.id }}"
tabindex="{% if forloop.first %}0{% else %}-1{% endif %}"
>{{ tab.title }}</button>
{% endfor %}
</div>
{% for tab in tabs %}
<div
role="tabpanel"
id="Panel-{{ tab.id }}"
aria-labelledby="Tab-{{ tab.id }}"
{% unless forloop.first %}hidden{% endunless %}
tabindex="0"
>{{ tab.content }}</div>
{% endfor %}- Arrow keys navigate between tabs (left/right)
- Only active tab has
tabindex="0", others-1
Dropdown Navigation
<nav aria-label="{{ 'accessibility.main_navigation' | t }}">
<ul role="list">
{% for link in linklists.main-menu.links %}
<li>
{% if link.links.size > 0 %}
<button aria-expanded="false" aria-controls="Submenu-{{ forloop.index }}">
{{ link.title }}
</button>
<ul id="Submenu-{{ forloop.index }}" hidden role="list">
{% for child in link.links %}
<li><a href="{{ child.url }}">{{ child.title }}</a></li>
{% endfor %}
</ul>
{% else %}
<a href="{{ link.url }}">{{ link.title }}</a>
{% endif %}
</li>
{% endfor %}
</ul>
</nav>Tooltip
<button aria-describedby="Tooltip-{{ block.id }}">
{{ 'labels.info' | t }}
</button>
<div id="Tooltip-{{ block.id }}" role="tooltip" popover>
{{ block.settings.tooltip_text }}
</div>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.
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
- Component accessibility patterns
- Focus and keyboard patterns
Component Accessibility Patterns
Detailed ARIA patterns for e-commerce components beyond what's covered in SKILL.md.
Color Swatches
<fieldset>
<legend>{{ 'products.color' | t }}</legend>
{% for color in product.options_by_name['Color'].values %}
<button
type="button"
role="radio"
aria-checked="{% if color == selected_color %}true{% else %}false{% endif %}"
aria-label="{{ color }}"
style="background-color: {{ color | handleize }}"
class="color-swatch"
>
<span class="visually-hidden">{{ color }}</span>
</button>
{% endfor %}
</fieldset>- Use
role="radio"witharia-checkedfor single-select swatches - Always provide text label (visually hidden if needed)
- Never rely on color alone — include text or pattern
Breadcrumbs
<nav aria-label="{{ 'accessibility.breadcrumb' | t }}">
<ol role="list">
<li><a href="/">{{ 'general.home' | t }}</a></li>
<li><a href="{{ collection.url }}">{{ collection.title }}</a></li>
<li aria-current="page">{{ product.title }}</li>
</ol>
</nav>aria-current="page"on the current page (last item, no link)- Use
<ol>for ordered list semantics
Tables (Size Charts, Specs)
<table>
<caption class="visually-hidden">{{ 'products.size_chart' | t }}</caption>
<thead>
<tr>
<th scope="col">{{ 'products.size' | t }}</th>
<th scope="col">{{ 'products.chest' | t }}</th>
<th scope="col">{{ 'products.waist' | t }}</th>
</tr>
</thead>
<tbody>
{% for row in size_data %}
<tr>
<th scope="row">{{ row.size }}</th>
<td>{{ row.chest }}</td>
<td>{{ row.waist }}</td>
</tr>
{% endfor %}
</tbody>
</table>- Always use
<th scope="col|row">for header cells <caption>describes the table purpose- Wrap in scrollable container for mobile:
<div role="region" tabindex="0" aria-label="...">
Slider / Range Input
<div role="group" aria-label="{{ 'filters.price_range' | t }}">
<label for="PriceMin-{{ section.id }}">{{ 'filters.min_price' | t }}</label>
<input
type="range"
id="PriceMin-{{ section.id }}"
min="0"
max="{{ max_price }}"
value="{{ min_value }}"
aria-valuemin="0"
aria-valuemax="{{ max_price }}"
aria-valuenow="{{ min_value }}"
aria-valuetext="{{ min_value | money }}"
>
<label for="PriceMax-{{ section.id }}">{{ 'filters.max_price' | t }}</label>
<input
type="range"
id="PriceMax-{{ section.id }}"
min="0"
max="{{ max_price }}"
value="{{ max_value }}"
aria-valuetext="{{ max_value | money }}"
>
</div>aria-valuetextprovides human-readable value (e.g., "$25.00" instead of "2500")
Switch / Toggle
<button
role="switch"
aria-checked="false"
aria-label="{{ 'settings.dark_mode' | t }}"
>
<span class="switch__thumb"></span>
</button>role="switch"witharia-checked="true|false"- Toggle with Space or Enter key
Combobox / Autocomplete
<div class="combobox">
<label for="Search-{{ section.id }}">{{ 'search.label' | t }}</label>
<input
type="text"
id="Search-{{ section.id }}"
role="combobox"
aria-expanded="false"
aria-autocomplete="list"
aria-controls="SearchResults-{{ section.id }}"
aria-activedescendant=""
>
<ul id="SearchResults-{{ section.id }}" role="listbox" hidden>
<!-- Suggestions populated via JS -->
</ul>
</div>- Arrow keys navigate suggestions, update
aria-activedescendant - Escape clears, Enter selects
aria-expandedreflects listbox visibility
Disclosure (Show/Hide)
<button
type="button"
aria-expanded="false"
aria-controls="Content-{{ block.id }}"
>
{{ block.settings.label }}
</button>
<div id="Content-{{ block.id }}" hidden>
{{ block.settings.content }}
</div>Simple show/hide toggle. If the content is a list of items (like a menu), prefer <details>/<summary>.
Product Media Gallery
<div role="region" aria-label="{{ 'products.media_gallery' | t }}">
<div class="gallery__main" aria-live="polite">
<img
id="MainImage-{{ section.id }}"
src="{{ current_image | image_url: width: 800 }}"
alt="{{ current_image.alt | escape }}"
>
</div>
<div class="gallery__thumbnails" role="list">
{% for media in product.media %}
<button
role="listitem"
aria-current="{% if forloop.first %}true{% else %}false{% endif %}"
aria-label="{{ 'products.view_image_n' | t: n: forloop.index }}"
>
<img
src="{{ media | image_url: width: 100 }}"
alt=""
loading="lazy"
>
</button>
{% endfor %}
</div>
</div>aria-current="true"on active thumbnail- Empty
alt=""on thumbnails (label viaaria-label) aria-live="polite"on main image container
Flip Card
<div class="flip-card" tabindex="0" aria-label="{{ 'general.flip_to_reveal' | t }}">
<div class="flip-card__front" aria-hidden="false">
<!-- Front content -->
</div>
<div class="flip-card__back" aria-hidden="true">
<!-- Back content -->
</div>
</div>- Both sides must be accessible (toggle
aria-hidden) - Respect
prefers-reduced-motion: instant flip instead of animation - Keyboard: Enter/Space to flip
Live Regions for Dynamic Updates
<!-- Cart count -->
<span aria-live="polite" aria-atomic="true" class="visually-hidden">
{{ 'cart.item_count' | t: count: cart.item_count }}
</span>
<!-- Filter results count -->
<div aria-live="polite" aria-atomic="true">
{{ 'filters.showing_results' | t: count: results.size }}
</div>
<!-- Form success -->
<div role="status" aria-live="polite">
{{ 'forms.success_message' | t }}
</div>aria-live="polite"for non-urgent updates (cart count, filter results)role="alert"for errors (implicitlyaria-live="assertive")aria-atomic="true"to read entire region on update
Focus & Keyboard Patterns
Focus Order Principles
1. DOM order = tab order — never use positive tabindex values 2. tabindex="0" makes non-interactive elements focusable (use sparingly) 3. tabindex="-1" removes from tab order but allows programmatic focus 4. Never reorder focus with CSS (order, flex-direction: row-reverse) without matching DOM order
Focus Trapping
class FocusTrap {
#focusableSelector = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
trap(container) {
const focusable = container.querySelectorAll(this.#focusableSelector);
const first = focusable[0];
const last = focusable[focusable.length - 1];
container.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
first?.focus();
}
}Focus Management Patterns
Opening a Modal/Drawer
openModal(trigger) {
this.lastFocusedElement = trigger; // Save return target
this.dialog.showModal();
const firstFocusable = this.dialog.querySelector(
'button, [href], input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable?.focus();
}
closeModal() {
this.dialog.close();
this.lastFocusedElement?.focus(); // Return focus
}Dynamic Content Updates
When content is loaded dynamically (AJAX filtering, infinite scroll):
async loadContent(url) {
const response = await fetch(url);
const html = await response.text();
this.container.innerHTML = html;
// Announce to screen readers
this.liveRegion.textContent = `${resultCount} results loaded`;
// Move focus to results (not back to filter)
const firstResult = this.container.querySelector('.result-item');
firstResult?.focus();
}Removing an Item
When an item is removed from a list (cart items, wishlist):
removeItem(item) {
const nextItem = item.nextElementSibling || item.previousElementSibling;
item.remove();
if (nextItem) {
nextItem.querySelector('button, a')?.focus();
} else {
// List is empty — focus the empty state or heading
this.emptyMessage?.focus();
}
}Keyboard Shortcuts by Component
Tab List
| Key | Action |
|---|---|
| Left/Right Arrow | Move between tabs |
| Home | First tab |
| End | Last tab |
| Enter/Space | Activate tab |
Carousel
| Key | Action |
|---|---|
| Left/Right Arrow | Previous/next slide |
| Enter/Space | Pause/resume auto-rotation |
Combobox
| Key | Action |
|---|---|
| Down Arrow | Open listbox / next option |
| Up Arrow | Previous option |
| Enter | Select current option |
| Escape | Close listbox |
Modal/Dialog
| Key | Action |
|---|---|
| Escape | Close |
| Tab | Cycle through focusable elements (trapped) |
| Shift+Tab | Reverse cycle |
Dropdown Menu
| Key | Action |
|---|---|
| Enter/Space | Open submenu |
| Escape | Close submenu |
| Arrow keys | Navigate items |
Roving Tabindex Pattern
For widget groups where only one item should be in tab order:
class TabList {
#tabs;
#activeIndex = 0;
handleKeydown(event) {
const { key } = event;
let newIndex = this.#activeIndex;
if (key === 'ArrowRight') newIndex++;
else if (key === 'ArrowLeft') newIndex--;
else if (key === 'Home') newIndex = 0;
else if (key === 'End') newIndex = this.#tabs.length - 1;
else return;
event.preventDefault();
// Wrap around
newIndex = (newIndex + this.#tabs.length) % this.#tabs.length;
// Update tabindex
this.#tabs[this.#activeIndex].tabIndex = -1;
this.#tabs[newIndex].tabIndex = 0;
this.#tabs[newIndex].focus();
this.#activeIndex = newIndex;
}
}Use for: tab lists, radio groups, toolbars, menu bars.
Screen Reader Announcements
Pattern: Live Region Update
announce(message) {
// Use existing live region
const region = document.querySelector('[aria-live="polite"]');
if (!region) return;
// Clear and re-set to trigger announcement
region.textContent = '';
requestAnimationFrame(() => {
region.textContent = message;
});
}When to Announce
| Event | Urgency | Method |
|---|---|---|
| Cart item added | Polite | aria-live="polite" |
| Form error | Assertive | role="alert" |
| Filter results count | Polite | aria-live="polite" + aria-atomic="true" |
| Page loaded (SPA) | Polite | Update <title> + announce |
| Countdown timer | Polite | Update every 30s, not every second |
Related skills
Forks & variants (1)
Liquid Theme A11y has 1 known copy in the catalog totaling 110 installs. They canonicalize to this original listing.
- shopify - 110 installs
How it compares
Choose liquid-theme-a11y over generic web accessibility skills when the markup language is Shopify Liquid for e-commerce theme components.
FAQ
What is liquid-theme-a11y?
Implement WCAG 2.2 accessibility patterns in Shopify Liquid themes. Covers e-commerce-specific components including product cards, carousels, cart drawers, price display, forms, fi
When should I use liquid-theme-a11y?
Implement WCAG 2.2 accessibility patterns in Shopify Liquid themes. Covers e-commerce-specific components including product cards, carousels, cart drawers, price display, forms, fi
Is liquid-theme-a11y safe to install?
Review the Security Audits panel on this page before production use.