Blog post title 'Fix Missing Alt Text in Shopify Themes: Complete Liquid Template Guide' with AltText.ai logo and Shopify shopping bag icon with broken image icons on dark background

Fix Missing Alt Text in Shopify Themes: Complete Liquid Template Guide

You added alt text in Shopify admin, but SEO audits still show empty alt attributes. Here's how to fix your theme templates.

Shopify Accessibility

You wrote alt text for every product image in your Shopify admin. You checked the image editor, filled in the field, saved. Then you ran an SEO audit and every image shows alt="". Hundreds of accessibility errors.

Shopify stores your alt text correctly—the problem is your theme templates. Dawn (Shopify's default theme) shipped with hardcoded alt="" in collection banners from v7 through v13, and in some product image templates through v15. Shopify has since patched Dawn, but many stores still run older versions—and third-party themes often have the same problem. The fix takes 10 minutes once you know where to look.

Find the Problem

Right-click any image on your storefront and select "Inspect." Look at the img tag:

<!-- This means your theme is ignoring your alt text -->
<img src="product-image.jpg" alt="">

<!-- This is what it should look like -->
<img src="product-image.jpg" alt="Blue cotton t-shirt front view">

Check your homepage (collection cards), collection pages (banners), product pages (featured images and galleries), and blog posts (article images). If DevTools shows alt="" but you entered alt text in the admin, you have a template problem.

The Fix Pattern

Every fix in this guide follows the same pattern. Here's a collection banner as an example. The template lives in sections/main-collection-banner.liquid.

The bug—hardcoded empty alt:

{% if collection.image %}
  <img
    src="{{ collection.image | image_url: width: 1920 }}"
    alt=""
    loading="lazy"
  >
{% endif %}

The fix—pull the stored alt text with a fallback:

{% if collection.image %}
  {%- assign banner_alt = collection.image.alt | default: collection.title -%}
  <img
    src="{{ collection.image | image_url: width: 1920 }}"
    alt="{{ banner_alt | escape }}"
    loading="lazy"
  >
{% endif %}

Three things happening here:

  • collection.image.alt pulls the alt text you entered in the admin
  • | default: collection.title falls back to the collection name if alt text is empty
  • | escape prevents XSS from quotes in user-entered text

That's it. Every fix below uses this same approach: replace hardcoded alt="" with the object's .alt property, add a default fallback, and escape the output.

Where to Apply It

Collection Cards

File: snippets/card-collection.liquid

{%- assign card_alt = collection.image.alt | default: collection.title -%}
{{ collection.image | image_url: width: 600 | image_tag:
  alt: card_alt,
  loading: 'lazy',
  class: 'collection-card__image'
}}

Product Cards and Featured Images

File: snippets/product-card.liquid or sections/main-product.liquid

{%- assign image_alt = product.featured_image.alt | default: product.title -%}
{{ product.featured_image | image_url: width: 800 | image_tag:
  alt: image_alt,
  loading: 'lazy',
  class: 'product-card__image'
}}

Product Gallery

Product galleries loop through product.media. Each media object has its own alt text ("Front view," "Side view," etc.):

{% for media in product.media %}
  {%- if media.media_type == 'image' -%}
    {%- assign media_alt = media.alt | default: product.title -%}
    {{ media | image_url: width: 1200 | image_tag:
      alt: media_alt,
      loading: 'lazy',
      class: 'product-media__image'
    }}
  {%- endif -%}
{% endfor %}

Variant Thumbnails

Color swatches and size option thumbnails need alt text too. Use cascading fallbacks—image alt, then variant title ("Blue / Medium"), then product title:

{% for variant in product.variants %}
  {% if variant.image %}
    {%- assign variant_alt = variant.image.alt
      | default: variant.title
      | default: product.title -%}
    <img
      src="{{ variant.image | image_url: width: 100 }}"
      alt="{{ variant_alt | escape }}"
      class="variant-thumbnail"
    >
  {% endif %}
{% endfor %}

Blog Article Images

File: sections/main-article.liquid

{% if article.image %}
  {%- assign article_image_alt = article.image.alt | default: article.title -%}
  {{ article.image | image_url: width: 1200 | image_tag:
    alt: article_image_alt,
    loading: 'lazy',
    class: 'article__featured-image'
  }}
{% endif %}

Watch Out: Inline Filters Inside image_tag

Liquid's parser can get confused when you chain filters like | default: directly inside image_tag parameters. The parser may apply the filter to the entire tag output instead of just the alt value. This is especially common with media.preview_image objects.

{% comment %} Risky - Liquid may misparse the | default: filter {% endcomment %}
{{ media.preview_image | image_url: width: 200 | image_tag:
  alt: media.alt | default: product.title
}}

{% comment %} Safe - assign handles the fallback first {% endcomment %}
{%- assign safe_alt = media.alt | default: product.title -%}
{{ media.preview_image | image_url: width: 200 | image_tag:
  alt: safe_alt
}}

Always use assign for fallback logic before passing to image_tag. It's more readable and avoids parsing surprises. Note that image_tag handles HTML escaping automatically, so you don't need | escape here—only add it when writing raw <img> tags.

Reusable Snippet

Tired of repeating yourself? Create snippets/image-with-alt.liquid:

{%- assign safe_alt = image.alt | default: fallback -%}
<img
  src="{{ image | image_url: width: width }}"
  alt="{{ safe_alt | escape }}"
  loading="lazy"
  {% if css_class %}class="{{ css_class }}"{% endif %}
  width="{{ image.width }}"
  height="{{ image.height }}"
>

Then use it everywhere:

{% render 'image-with-alt',
  image: collection.image,
  fallback: collection.title,
  width: 1920,
  css_class: 'collection-banner__image'
%}

{% render 'image-with-alt',
  image: product.featured_image,
  fallback: product.title,
  width: 600,
  css_class: 'product-card__image'
%}

One file to maintain instead of fixing the same pattern in dozens of templates.

Test Your Fixes

  1. Preview first: Use Shopify's theme editor preview mode before going live
  2. Inspect with DevTools: Check that alt attributes now show your admin text on collection pages, product pages, and blog posts
  3. Test fallbacks: Create a test product with no alt text to confirm your default logic works
  4. Re-run your audit: Lighthouse, Screaming Frog, or AltText.ai's analyzer to confirm the errors are gone

Now You Need Actual Alt Text

Fixing templates makes Shopify output whatever you enter in the admin. But if your alt text fields are empty, you've just replaced alt="" with... alt="". The template fix and the content are two separate problems.

For stores with hundreds or thousands of products, writing alt text by hand takes days. The AltText.ai Shopify app generates descriptive alt text automatically when you upload product images and can bulk-process your existing catalog. Once your templates are fixed and your images have real descriptions, your store is accessible to screen readers and performs better in Google image search.

Fix the templates, then let us handle the alt text

The AltText.ai Shopify app generates accurate, SEO-friendly descriptions for every product image. Bulk-process your existing catalog or auto-generate on upload.