Back to Blog
Shopify Form Accessibility: Labels, Errors, Autocomplete in Liquid — featured image

Shopify Form Accessibility: Labels, Errors, Autocomplete in Liquid

Author avatarVijaygopal Balasa
9 min read

Forms are a high-impact accessibility review area because unclear labels, unidentified purposes, visual-only errors, and missing recovery guidance can block signup or purchase tasks. The examples below illustrate useful Liquid patterns, but they are not a claim that every theme has the same failure or that one snippet fixes every form state. Test the rendered form, validation behavior, keyboard flow, and screen-reader announcements on the actual storefront.

Why "placeholder only" is not enough

The most common Shopify form failure:

{%- comment -%} Wrong — placeholder used as label {%- endcomment -%}
<form action="/contact#newsletter" method="post">
  <input type="hidden" name="form_type" value="customer">
  <input type="email" name="contact[email]" placeholder="Email">
  <button type="submit">Subscribe</button>
</form>

Three problems:

  1. No programmatic label. Screen readers announce the input as "edit text" with no field-name context.
  2. No autocomplete. Browser autofill cannot pre-populate the email field.
  3. Placeholder vanishes when the user types, so users with cognitive disabilities who lose track of what field they're in have no recovery cue.

The right pattern — visible label + autocomplete + label-input association

<form action="/contact#newsletter" method="post" class="newsletter-form">
  <input type="hidden" name="form_type" value="customer">
  <input type="hidden" name="contact[tags]" value="newsletter">
  
  <label for="newsletter-email">Email</label>
  <input
    type="email"
    id="newsletter-email"
    name="contact[email]"
    autocomplete="email"
    required
    aria-describedby="newsletter-email-help">
  
  <p id="newsletter-email-help" class="form-help">
    We\'ll email occasional accessibility-compliance updates. Unsubscribe anytime.
  </p>
  
  <button type="submit">Subscribe</button>
</form>

Key elements:

  • <label for="newsletter-email"> paired with id="newsletter-email" on the input — the canonical programmatic association.
  • autocomplete="email" — declares the input purpose for autofill + accessibility extensions.
  • aria-describedby="newsletter-email-help" — connects helper text to the input so screen readers read it after the field name.
  • The label is visible, not hidden in .sr-only — visible labels help everyone, not just screen-reader users.

If brand design genuinely requires the label to be hidden (a single-field newsletter where space is constrained), use .sr-only rather than removing the label entirely:

<label for="newsletter-email" class="sr-only">Email</label>
<input
  type="email"
  id="newsletter-email"
  name="contact[email]"
  placeholder="Email"
  autocomplete="email"
  required>

The placeholder is now a visual fallback for sighted users; the screen-reader user gets the proper label.

The autocomplete value table — what to use where

WCAG 1.3.5 references the HTML Living Standard's autocomplete value list. The values you'll use most on Shopify forms:

Fieldautocomplete value
Emailemail
Phonetel
Phone (country code)tel-country-code
Full namename
First namegiven-name
Last namefamily-name
Street address line 1address-line1
Street address line 2address-line2
Cityaddress-level2
State / regionaddress-level1
Postal / ZIP codepostal-code
Countrycountry
Country codecountry-name
Birthdaybday
New passwordnew-password
Existing passwordcurrent-password
One-time codeone-time-code
Credit card numbercc-number
Credit card expiry monthcc-exp-month
Credit card expiry yearcc-exp-year
Credit card security codecc-csc

Shopify's native checkout sets these correctly. Custom storefront forms (Hydrogen, custom signup, third-party app embeds) often miss them — audit every form on the storefront, not just the native checkout.

Glossary: HTML autocomplete attribute →

Error identification — the right way

Setup: a customer signs up with an existing email. The form should surface the error so screen-reader users know what failed.

The wrong way — color-only:

{%- if form.errors -%}
  <input type="email" name="contact[email]" class="error-red-border">
{%- endif -%}

Red border tells sighted users something is wrong. Tells screen-reader users nothing.

The right way — aria-invalid + aria-describedby + role="alert":

{%- assign has_error = form.errors and form.errors.email -%}
<label for="signup-email">Email</label>
<input
  type="email"
  id="signup-email"
  name="customer[email]"
  autocomplete="email"
  required
  {% if has_error %}aria-invalid="true" aria-describedby="signup-email-error"{% endif %}>
{%- if has_error -%}
  <p id="signup-email-error" role="alert" class="form-error">
    {{ form.errors.translated_fields.email }} {{ form.errors.messages.email }}.
    Try a different email or sign in if you already have an account.
  </p>
{%- endif -%}

Three things happen:

  1. aria-invalid="true" flags the field as invalid for screen readers.
  2. aria-describedby connects the input to the error message so the message is announced after the field name.
  3. role="alert" tells the screen reader to announce the error immediately when it appears (implicit aria-live="assertive").

The error text itself satisfies WCAG 3.3.3 Error Suggestion — it explains what went wrong AND how to fix it.

Common Shopify form-accessibility failures

1. Newsletter signup with placeholder-only label

Cause: theme <input placeholder="Email"> with no label. Fix: add <label for> + matching id + autocomplete="email".

2. Search bar with no accessible name

Cause: theme renders <input type="search"> with no label or aria-label. Fix: <label for="search-input" class="sr-only">Search</label> or aria-label="Search".

3. Quantity stepper with no label

Cause: cart line-item quantity input is <input type="number"> with no label. Fix: <label for="qty-{{ line.key }}" class="sr-only">Quantity</label> so screen readers know which line item the field controls.

4. Checkout custom field added via cart-attribute script

Cause: merchant adds gift-message or order-note input via Liquid; ships without label. Fix: every custom cart-attribute or line-item-property field needs <label> + autocomplete (where applicable).

5. Customer-account login that blocks paste

Cause: theme has onpaste="return false" on the password field (often inherited from a theme-customization tutorial). Fix: remove onpaste and autocomplete="off"; the password field should accept paste so password managers work. WCAG 3.3.8 Accessible Authentication explicit requirement at WCAG 2.2 AA.

6. Form errors flagged by color only

Cause: stylesheet adds border-color: red on .error class but no text content. Fix: add the aria-invalid + aria-describedby + role="alert" pattern above.

The full Liquid newsletter pattern

{%- form 'customer', id: 'newsletter-form', class: 'newsletter-form' -%}
  <input type="hidden" name="contact[tags]" value="newsletter">
  
  <label for="newsletter-email">{{ 'general.newsletter.email_label' | t }}</label>
  <div class="newsletter-form__input-wrap">
    <input
      type="email"
      id="newsletter-email"
      name="contact[email]"
      autocomplete="email"
      placeholder="{{ 'general.newsletter.email_placeholder' | t }}"
      required
      aria-required="true"
      {% if form.errors %}aria-invalid="true" aria-describedby="newsletter-email-error"{% endif %}>
    <button type="submit">
      {{ 'general.newsletter.submit' | t }}
    </button>
  </div>
  
  {%- if form.errors -%}
    <p id="newsletter-email-error" role="alert" class="newsletter-form__error">
      {{- form.errors | default_errors -}}
    </p>
  {%- elsif form.posted_successfully? -%}
    <p role="status" class="newsletter-form__success">
      {{ 'general.newsletter.success' | t }}
    </p>
  {%- endif -%}
{%- endform -%}

The role="status" on the success message is the WCAG 4.1.3 Status Messages compliance for the success path.

Quick checklist

  • Every <input>, <select>, <textarea> has a <label for> + matching id.
  • Every user-info field has a correct autocomplete value.
  • Required fields have both required + aria-required="true".
  • Error messages use aria-invalid="true" on the input + aria-describedby linking to the error + role="alert".
  • Error messages explain what went wrong AND how to fix it.
  • Success messages use role="status" so screen readers announce them.
  • Password fields do not block paste (onpaste="return false" removed).
  • Search inputs have <label class="sr-only"> or aria-label.
  • Cart-attribute / line-item-property custom fields have labels.

Verify your fix

Run the free public AccessComply scan on the storefront. It checks up to 10 discoverable pages, subject to crawler safety and time limits, and reports detected form-label findings with selectors and WCAG mappings. Public scans require no signup and are limited to 3 per day per store. The installed Free app separately includes 3 app scans per calendar month and 3 eligible deterministic fixes per rolling 30 days.

Further reading

Free scan available

Find the storefront issues holding back growth

Scan SEO, speed, and accessibility by page. Review supported fixes before they run, keep saved originals, and verify the live result afterward.

Vijaygopal Balasa, Founder, AccessComply
Written by

Vijaygopal Balasa

Founder, AccessComply

Founder of AccessComply. Builds tools that find and fix supported Shopify accessibility issues in theme code—not through overlays. Focused on practical WCAG 2.2 AA improvements for merchants.

Keep improving after this fix.

Get concise, practical updates on Shopify SEO, speed, accessibility, and safer storefront changes. No daily noise; unsubscribe anytime.

More on Shopify How-To

See all →