Skip to content

Accessibility

An accessible theme is one every seller can ship without worrying who it excludes. Most of it comes from using the right HTML.

Start with semantic HTML

A <button> is focusable, activates on Enter and Space, and announces itself as a button. A <div> with a click handler does none of that. Use the element that means what you're building: <button>, <a>, <nav>, <main>, <header>, <footer>, <ul>, <label>.

Set the section's tag in its schema so the wrapper carries meaning too:

json
{ "label": "t:sections.footer.label", "tag": "footer" }

Available values are article, aside, div, footer, header, and section.

One main landmark

The layout should have exactly one <main>, wrapping the content_for_layout object, with the header and footer outside it. That gives assistive technology a reliable way to skip navigation.

liquid
<body>
  {% section 'header' %}

  <main id="main">
    {{ content_for_layout }}
  </main>

  {% section 'footer' %}
</body>

Pair it with a skip link as the first focusable element on the page.

Headings in order

One <h1> per page, then <h2> and below in sequence without skipping levels. Sections are reordered by sellers, so don't hard-code an <h1> into a section that can appear anywhere. Let the setting decide the text and the template decide the level, or expose the level as a setting.

Images need honest alt text

Describe what the image conveys. Decorative images take an empty alt so screen readers skip them rather than reading a filename.

liquid
<img src="{{ product.preview_image.medium }}" alt="{{ product.title }}">

<img src="{{ 'divider.svg' | asset_url }}" alt="">

Seller-uploaded images carry their own alt. Use it, and fall back to something meaningful rather than to nothing:

liquid
<img
  src="{{ section.settings.image.src }}"
  alt="{{ section.settings.image.alt | default: section.settings.heading }}"
>

Label every control

Icon-only buttons have no accessible name unless you give them one:

liquid
<button aria-label="{{ 'cart.open' | t }}">
  {% render 'misc.icon', name: 'cart' %}
</button>

Form inputs need a real <label> tied by for and id. A placeholder is not a label; it disappears as soon as the customer types.

Use aria-* to describe state that HTML can't: aria-expanded on a disclosure, aria-current="page" on the active nav link, aria-selected on the active tab, aria-invalid on a field that failed validation. Keep them in sync from JavaScript when the state changes.

Keyboard and focus

Everything clickable must be reachable and operable by keyboard. Check by tabbing through the page.

  • Never remove focus outlines without replacing them. outline: none with nothing in its place makes the theme unusable for keyboard navigation.
  • Move focus into drawers and modals when they open, trap it while they're open, and return it to the trigger on close.
  • Keep the DOM order matching the visual order. Reordering with CSS order or flex-direction: row-reverse desynchronizes the tab sequence.

Contrast and motion

Sellers pick the colours, so the theme has to hold up across their choices. Check text against its background at the defaults you ship, keep body text at 4.5:1 or better, and never rely on colour alone to communicate state. Add an icon or text to an error, not just a red border.

Respect the system preference for reduced motion:

css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Language and direction

Set lang and dir on the <html> element from the store's locale. Arabic storefronts need rtl, and getting this right fixes text alignment, list markers, and scroll direction at once.

liquid
{% assign lang = store.store_locale.iso_code %}
{% assign direction = 'ltr' %}
{% if lang == 'ar' %}
  {% assign direction = 'rtl' %}
{% endif %}

<html lang="{{ lang }}" dir="{{ direction }}">

Build layouts with logical CSS properties (margin-inline-start, padding-inline, inset-inline) rather than left and right, so they flip automatically.

Touch targets

Interactive elements should be at least 48 by 48 pixels, with spacing between adjacent targets. Most storefront traffic is mobile, and undersized controls fail everyone, not only customers with motor impairments.