Blog

Shopify Liquid Accessibility: Developer Guide to Building Accessible Themes

TestParty
TestParty
December 27, 2025

Building an accessible Shopify store starts at the code level. While many merchants focus on choosing the right theme or installing accessibility apps, developers know that true accessibility comes from writing semantic, well-structured Liquid templates. This guide provides everything you need to know about implementing accessibility in Shopify's Liquid templating language.

Whether you're building a custom theme from scratch or modifying an existing one, understanding how to properly implement ARIA attributes, semantic HTML, and accessible component patterns in Liquid will help you create stores that work for everyone.

Key Takeaways

  • Liquid objects provide accessibility data - Use built-in objects like `image.alt` and `product.title` to populate accessible content automatically
  • ARIA attributes require careful implementation - Dynamic ARIA states must update correctly when Liquid renders server-side
  • Semantic HTML is foundational - Proper heading hierarchy and landmark regions matter more than ARIA overlays
  • Component accessibility follows patterns - Common Shopify components like product cards and navigation have established accessibility patterns
  • Testing requires multiple approaches - Liquid accessibility needs both automated scanning and manual testing with assistive technology

Understanding Liquid's Role in Accessibility

Shopify's Liquid templating language generates the HTML that browsers and assistive technologies consume. Every accessibility decision you make in your Liquid code directly impacts users who rely on screen readers, keyboard navigation, or other assistive technologies.

Unlike client-side JavaScript frameworks where accessibility is often an afterthought, Liquid gives you the opportunity to bake accessibility into your markup from the start. The HTML that Liquid outputs is what gets indexed by search engines and consumed by accessibility tools.

How Liquid Objects Support Accessibility

Shopify provides several built-in objects that support accessibility out of the box:

{% comment %} Product images with alt text {% endcomment %}
<img
  src="{{ product.featured_image | image_url: width: 600 }}"
  alt="{{ product.featured_image.alt | escape }}"
  loading="lazy"
  width="{{ product.featured_image.width }}"
  height="{{ product.featured_image.height }}"
>

{% comment %} Always provide fallback alt text {% endcomment %}
{% assign alt_text = image.alt | default: product.title %}
<img src="{{ image | image_url }}" alt="{{ alt_text | escape }}">

The `escape` filter is critical here. It prevents special characters in alt text from breaking your HTML or creating security vulnerabilities.

Implementing ARIA in Liquid Templates

ARIA (Accessible Rich Internet Applications) attributes enhance HTML semantics when native elements cannot convey the necessary information. In Liquid, implementing ARIA requires understanding both the static and dynamic aspects of your components.

Static ARIA Attributes

For elements that do not change state, implement ARIA attributes directly in your Liquid:

{% comment %} Navigation landmark with label {% endcomment %}
<nav aria-label="{{ 'accessibility.main_navigation' | t }}">
  {% render 'header-menu' %}
</nav>

{% comment %} Product grid with role {% endcomment %}
<ul role="list" aria-label="{{ 'accessibility.product_list' | t }}">
  {% for product in collection.products %}
    <li>{% render 'product-card', product: product %}</li>
  {% endfor %}
</ul>

Using translation keys (`| t` filter) for ARIA labels enables multilingual accessibility support.

Dynamic ARIA States

Components with interactive states require JavaScript to update ARIA attributes, but Liquid sets the initial state:

{% comment %} Accordion component with proper initial state {% endcomment %}
<div class="accordion">
  {% for block in section.blocks %}
    <h3>
      <button
        type="button"
        aria-expanded="false"
        aria-controls="accordion-panel-{{ block.id }}"
        class="accordion__trigger"
      >
        {{ block.settings.title }}
      </button>
    </h3>
    <div
      id="accordion-panel-{{ block.id }}"
      role="region"
      aria-labelledby="accordion-heading-{{ block.id }}"
      hidden
    >
      {{ block.settings.content }}
    </div>
  {% endfor %}
</div>

The JavaScript handling these interactions must update `aria-expanded` and the `hidden` attribute when users toggle panels.

Building Accessible Liquid Components

Let's examine how to build common Shopify components with accessibility in mind.

Accessible Product Cards

Product cards appear throughout Shopify stores. Here's an accessible implementation:

{% comment %} snippets/product-card.liquid {% endcomment %}
<article class="product-card" aria-labelledby="product-title-{{ product.id }}">
  <a href="{{ product.url }}" class="product-card__link">
    <div class="product-card__media">
      {% if product.featured_image %}
        <img
          src="{{ product.featured_image | image_url: width: 400 }}"
          alt="{{ product.featured_image.alt | default: product.title | escape }}"
          width="400"
          height="{{ 400 | divided_by: product.featured_image.aspect_ratio | round }}"
          loading="lazy"
        >
      {% else %}
        <div class="product-card__placeholder" aria-hidden="true">
          {{ 'product-1' | placeholder_svg_tag }}
        </div>
      {% endif %}
    </div>

    <div class="product-card__content">
      <h3 id="product-title-{{ product.id }}" class="product-card__title">
        {{ product.title }}
      </h3>

      <p class="product-card__price">
        <span class="visually-hidden">{{ 'accessibility.price' | t }}</span>
        {{ product.price | money }}
        {% if product.compare_at_price > product.price %}
          <span class="visually-hidden">{{ 'accessibility.original_price' | t }}</span>
          <s class="product-card__compare-price">{{ product.compare_at_price | money }}</s>
          <span class="visually-hidden">{{ 'accessibility.sale' | t }}</span>
        {% endif %}
      </p>
    </div>
  </a>
</article>

Key accessibility features in this component:

  • `article` element provides semantic grouping
  • `aria-labelledby` connects the card to its title
  • Fallback alt text uses product title
  • Price information includes screen reader context
  • Sale pricing is announced properly

Accessible Variant Selectors

Variant selectors present unique accessibility challenges. Here's an accessible approach:

{% comment %} snippets/variant-selector.liquid {% endcomment %}
<fieldset class="variant-selector">
  <legend class="variant-selector__legend">
    {{ option.name }}
    <span class="variant-selector__selected">: {{ option.selected_value }}</span>
  </legend>

  <div class="variant-selector__options" role="radiogroup">
    {% for value in option.values %}
      {% assign variant_available = true %}
      {% comment %} Check availability logic here {% endcomment %}

      <label
        class="variant-selector__option {% unless variant_available %}variant-selector__option--unavailable{% endunless %}"
        {% unless variant_available %}aria-disabled="true"{% endunless %}
      >
        <input
          type="radio"
          name="{{ option.name }}"
          value="{{ value | escape }}"
          {% if option.selected_value == value %}checked{% endif %}
          {% unless variant_available %}disabled{% endunless %}
          class="visually-hidden"
        >
        <span class="variant-selector__label">
          {{ value }}
          {% unless variant_available %}
            <span class="visually-hidden">{{ 'accessibility.unavailable' | t }}</span>
          {% endunless %}
        </span>
      </label>
    {% endfor %}
  </div>
</fieldset>

Accessible Quantity Inputs

Quantity selectors need clear labeling and keyboard support:

{% comment %} snippets/quantity-input.liquid {% endcomment %}
<div class="quantity-input">
  <label for="quantity-{{ product.id }}" class="visually-hidden">
    {{ 'accessibility.quantity' | t }}
  </label>

  <button
    type="button"
    class="quantity-input__button"
    aria-label="{{ 'accessibility.decrease_quantity' | t: product: product.title }}"
    data-action="decrease"
  >
    <span aria-hidden="true">-</span>
  </button>

  <input
    type="number"
    id="quantity-{{ product.id }}"
    name="quantity"
    value="1"
    min="1"
    max="{{ product.selected_or_first_available_variant.inventory_quantity | default: 99 }}"
    class="quantity-input__field"
  >

  <button
    type="button"
    class="quantity-input__button"
    aria-label="{{ 'accessibility.increase_quantity' | t: product: product.title }}"
    data-action="increase"
  >
    <span aria-hidden="true">+</span>
  </button>
</div>

Semantic HTML Best Practices in Liquid

Beyond ARIA, semantic HTML provides the foundation for accessibility. Here's how to structure your Liquid templates semantically.

Heading Hierarchy

Maintain proper heading levels throughout your templates:

{% comment %} layout/theme.liquid - establish h1 {% endcomment %}
<main id="main-content" role="main">
  {% if template == 'index' %}
    <h1 class="visually-hidden">{{ shop.name }}</h1>
  {% endif %}
  {{ content_for_layout }}
</main>

{% comment %} templates/product.liquid - h1 for product {% endcomment %}
<h1 class="product__title">{{ product.title }}</h1>

{% comment %} sections/product-recommendations.liquid - h2 for section {% endcomment %}
<section aria-labelledby="recommendations-heading">
  <h2 id="recommendations-heading">{{ section.settings.heading }}</h2>
  {% comment %} Products use h3 {% endcomment %}
</section>

Landmark Regions

Define clear landmark regions in your theme layout:

{% comment %} layout/theme.liquid {% endcomment %}
<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}">
<head>
  {% comment %} Head content {% endcomment %}
</head>
<body>
  <a href="#main-content" class="skip-link">
    {{ 'accessibility.skip_to_content' | t }}
  </a>

  <header role="banner">
    {% section 'header' %}
  </header>

  <main id="main-content" tabindex="-1">
    {{ content_for_layout }}
  </main>

  <footer role="contentinfo">
    {% section 'footer' %}
  </footer>
</body>
</html>

Form Accessibility in Liquid

Forms are critical conversion points that must be accessible:

{% comment %} snippets/contact-form.liquid {% endcomment %}
{% form 'contact', class: 'contact-form' %}
  {% if form.errors %}
    <div role="alert" class="form-errors">
      <h2>{{ 'accessibility.form_errors' | t }}</h2>
      <ul>
        {% for error in form.errors %}
          <li>
            <a href="#contact-{{ error.first }}">
              {{ error.first | capitalize }}: {{ error.last }}
            </a>
          </li>
        {% endfor %}
      </ul>
    </div>
  {% endif %}

  <div class="form-field">
    <label for="contact-name">
      {{ 'contact.form.name' | t }}
      <span aria-hidden="true">*</span>
      <span class="visually-hidden">{{ 'accessibility.required' | t }}</span>
    </label>
    <input
      type="text"
      id="contact-name"
      name="contact[name]"
      required
      aria-required="true"
      {% if form.errors contains 'name' %}
        aria-invalid="true"
        aria-describedby="name-error"
      {% endif %}
    >
    {% if form.errors contains 'name' %}
      <span id="name-error" class="form-error">{{ form.errors.name }}</span>
    {% endif %}
  </div>

  <button type="submit">{{ 'contact.form.submit' | t }}</button>
{% endform %}

Testing Liquid Accessibility

Validating accessibility in Liquid templates requires multiple testing approaches:

  1. HTML validation: Ensure your generated HTML is valid
  2. Automated scanning: Use tools to catch common issues
  3. Screen reader testing: Verify the experience with actual assistive technology
  4. Keyboard testing: Navigate your store using only a keyboard

For comprehensive testing across your entire Shopify store, automated accessibility tools can identify issues in your Liquid-generated HTML and help you fix them at the source code level.

Common Liquid Accessibility Mistakes

Avoid these frequent errors:

Empty alt attributes on decorative images: ```liquid {% comment %} Correct: decorative images get empty alt {% endcomment %} <img src="{{ 'decorative-border.svg' | asset_url }}" alt="" aria-hidden="true">

{% comment %} Wrong: missing alt attribute entirely {% endcomment %} <img src="{{ 'decorative-border.svg' | asset_url }}"> ```

Missing form labels: ```liquid {% comment %} Wrong: placeholder is not a label {% endcomment %} <input type="email" placeholder="Email">

{% comment %} Correct: proper label association {% endcomment %} <label for="email">Email</label> <input type="email" id="email"> ```

Inaccessible icons: ```liquid {% comment %} Correct: icon button with accessible name {% endcomment %} <button type="button" aria-label="{{ 'accessibility.close' | t }}"> {% render 'icon-close' %} </button> ```

Frequently Asked Questions

How do I add ARIA labels in Shopify Liquid?

Add ARIA attributes directly to your HTML elements in Liquid templates. Use the translation filter (`| t`) for labels that should support multiple languages: `aria-label="{{ 'accessibility.menu' | t }}"`.

Does Shopify Liquid support accessibility features natively?

Liquid provides the tools to build accessible markup, including access to image alt text, product information, and the ability to add any HTML attribute. However, accessibility must be intentionally implemented by developers.

How do I make Shopify product variants accessible?

Use semantic fieldset and legend elements with radio inputs for variants. Include availability status in screen reader text and ensure keyboard users can navigate between options.

What's the best way to handle dynamic content accessibility in Liquid?

For content that changes via JavaScript, set proper initial ARIA states in your Liquid code, then use JavaScript to update those states. Always include `aria-live` regions for content that updates without page reload.

How do I test accessibility in my Liquid theme?

Combine automated testing tools with manual screen reader testing. Generate HTML from your Liquid templates and validate it for accessibility issues. For ongoing monitoring, consider comprehensive Shopify accessibility solutions that scan your live store.


This article was written with AI assistance and reviewed by accessibility professionals at TestParty. Our platform helps Shopify merchants achieve accessibility compliance through source code remediation, fixing the actual HTML, CSS, and JavaScript rather than relying on overlay widgets that don't address underlying issues.

Stay informed

Accessibility insights delivered
straight to your inbox.

Contact Us

Automate the software work for accessibility compliance, end-to-end.

Empowering businesses with seamless digital accessibility solutions—simple, inclusive, effective.

Book a Demo