Images must have alternative text

Blind and low-vision customers rely on screen readers to hear what an image shows. Product photos, banners and logos without alternative text are announced as a file name or skipped, so customers cannot tell which colour, model or offer they are looking at and may abandon the purchase.

Rule id
image-alt
Source
axe-core 4.13.0
Impact
Critical
Fix tier
ASSISTED
on Shopify, Shopware, WooCommerce and Magento
Status
runs

What the check does

Ensure <img> elements have alternative text or a role of none or presentation

The rule tests every <img> that assistive technology can reach, skipping images hidden with display: none or aria-hidden="true" on the image or an ancestor. An <img> with another valid role is skipped too, unless that role requires an accessible name or the image is focusable.

An image passes with any of these: an alt attribute, even an empty alt=""; a non-empty aria-label; an aria-labelledby pointing to an element with text; a title with text; or a resolved role of none or presentation. That role is ignored on an image that is focusable or has a global ARIA attribute such as aria-live. An alt made only of whitespace fails unless the role is presentational.

By default Validiscan’s scanner runs the rule at a desktop and a mobile size, so an image hidden with display: none at one size is tested only at the other. The rule checks that a text alternative exists, not its wording; it does not decide whether an image is decorative or whether its text fits it.

How to fix it

Add an alt attribute to every <img>. For informative images and image links, describe what the image shows or does, such as the product name and colour variant; use alt="" only for purely decorative images. In themes, fill alt from the media alt text field in the shop admin with the product title as a fallback.

WCAG success criteria

EN 301 549 clauses

The EN 301 549 clause of each criterion, by version of the standard
WCAG criterion Version 3.2.1 Version 4.1.1
1.1.1 9.1.1.1 9.1.1.1

Recipes that list this rule

This rule has the tier ASSISTED. In a project, a finding of it takes the ASSISTED way, and these recipes are not applied to it.

Shopify: recipe decorative-image-alt

Show the before and after (1)
Before and after Example data

sections/footer.liquid Changes to sections/footer.liquid: 1 added, 1 removed

Changes to sections/footer.liquid: 1 added, 1 removed
Line before Line after Change Code
12 12 <input type="hidden" name="contact[tags]" value="newsletter">
13 13 <div class="newsletter-form__field-wrapper">
14 14 <input type="email" name="contact[email]" class="field__input" placeholder="{{ 'newsletter.label' | t }}" autocorrect="off" autocapitalize="off">
15 Removed <button type="submit" class="button newsletter-form__button" name="commit"><img src="{{ 'logo.svg' | asset_url }}" class="newsletter-form__icon" width="16" height="16">{{ 'newsletter.button' | t }}</button>
15 Added <button type="submit" class="button newsletter-form__button" name="commit"><img src="{{ 'logo.svg' | asset_url }}" class="newsletter-form__icon" width="16" height="16" alt="">{{ 'newsletter.button' | t }}</button>
16 16 </div>
17 17 {%- endform -%}
18 18 </div>

sections/header.liquid Changes to sections/header.liquid: 1 added, 1 removed

Changes to sections/header.liquid: 1 added, 1 removed
Line before Line after Change Code
46 46
47 47 <div class="header__icons">
48 48 {%- if section.settings.show_help_link %}
49 Removed <a href="{{ section.settings.help_link_url }}" class="header__utility-link link"><svg class="header__utility-icon" role="img" viewBox="0 0 24 24" width="16" height="16"><circle cx="12" cy="12" r="9"/></svg>{{ section.settings.help_link_label | escape }}</a>
49 Added <a href="{{ section.settings.help_link_url }}" class="header__utility-link link"><svg class="header__utility-icon" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/></svg>{{ section.settings.help_link_label | escape }}</a>
50 50 {%- endif %}
51 51 <a href="{{ routes.account_login_url }}" class="header__icon header__icon--account link">
52 52 {%- render 'icon', icon: 'account', size: icon_size -%}

Shopify: recipe icon-accessible-name

Show the before and after (2)
buttons Example data

locales/en.default.json Changes to locales/en.default.json: 3 added, 1 removed

Changes to locales/en.default.json: 3 added, 1 removed
Line before Line after Change Code
34 34 "total_reviews": "{{ count }} reviews",
35 35 "link_messages": {
36 36 "new_window": "Opens in a new window."
37 Removed }
37 Added },
38 Added "accept_cookies": "Accept cookies",
39 Added "reject_cookies": "Reject cookies"
38 40 },
39 41 "sections": {
40 42 "header": {

sections/header.liquid Changes to sections/header.liquid: 1 added, 0 removed

Changes to sections/header.liquid: 1 added, 0 removed
Line before Line after Change Code
40 40 <input class="search__input field__input" type="search" name="q" value="" placeholder="{{ 'general.search.placeholder' | t }}" tabindex="1">
41 41 <button type="submit" class="search__button">
42 42 {%- render 'icon', icon: 'search', size: 20 -%}
43 Added <span class="visually-hidden">{{ 'general.search.search' | t }}</span>
43 44 </button>
44 45 </form>
45 46 </div>

sections/main-product.liquid Changes to sections/main-product.liquid: 1 added, 1 removed

Changes to sections/main-product.liquid: 1 added, 1 removed
Line before Line after Change Code
88 88 <p class="form__label">{{ option.name | escape }}</p>
89 89 <div class="swatch-list swatch-list--compact">
90 90 {%- for swatch in option.swatches %}
91 Removed <button type="button" class="swatch swatch--button{% if swatch.selected %} swatch--active{% endif %}" style="--swatch-color: {{ swatch.color }};" data-option-index="{{ option_index }}" data-value="{{ swatch.value | escape }}"></button>
91 Added <button type="button" class="swatch swatch--button{% if swatch.selected %} swatch--active{% endif %}" style="--swatch-color: {{ swatch.color }};" data-option-index="{{ option_index }}" data-value="{{ swatch.value | escape }}"><span class="visually-hidden">{{ swatch.value | escape }}</span></button>
92 92 {%- endfor %}
93 93 </div>
94 94 </div>

sections/slideshow.liquid Changes to sections/slideshow.liquid: 2 added, 0 removed

Changes to sections/slideshow.liquid: 2 added, 0 removed
Line before Line after Change Code
59 59 <div class="slideshow__controls">
60 60 <button type="button" class="slider-button slider-button--prev" name="previous" data-slide-prev>
61 61 {%- render 'icon', icon: 'chevron-left', size: 20 -%}
62 Added <span class="visually-hidden">{{ 'accessibility.previous_slide' | t }}</span>
62 63 </button>
63 64 <div class="slider-counter">
64 65 {%- for block in section.blocks %}
67 68 </div>
68 69 <button type="button" class="slider-button slider-button--next" name="next" data-slide-next>
69 70 {%- render 'icon', icon: 'chevron-right', size: 20 -%}
71 Added <span class="visually-hidden">{{ 'accessibility.next_slide' | t }}</span>
70 72 </button>
71 73 </div>
72 74 </slideshow-component>

snippets/cookie-banner.liquid Changes to snippets/cookie-banner.liquid: 2 added, 0 removed

Changes to snippets/cookie-banner.liquid: 2 added, 0 removed
Line before Line after Change Code
23 23 <div class="cookie-banner__actions">
24 24 <button type="button" class="cookie-banner__icon-button cookie-banner__icon-button--accept" data-cookie-accept>
25 25 {%- render 'icon', icon: 'check', size: 20 -%}
26 Added <span class="visually-hidden">{{ 'accessibility.accept_cookies' | t }}</span>
26 27 </button>
27 28 <button type="button" class="cookie-banner__icon-button" data-cookie-reject>
28 29 {%- render 'icon', icon: 'close', size: 20 -%}
30 Added <span class="visually-hidden">{{ 'accessibility.reject_cookies' | t }}</span>
29 31 </button>
30 32 </div>
31 33 </div>

snippets/quantity-input.liquid Changes to snippets/quantity-input.liquid: 2 added, 0 removed

Changes to snippets/quantity-input.liquid: 2 added, 0 removed
Line before Line after Change Code
9 9 {%- endcomment -%}
10 10 <div class="quantity quantity--compact" data-quantity{% if line %} data-line="{{ line }}"{% endif %}><button class="quantity__button" name="minus" type="button" data-quantity-minus>
11 11 {%- render 'icon', icon: 'minus', size: 12 -%}
12 Added <span class="visually-hidden">{{ 'products.product.quantity.decrease' | t: product: product_title }}</span>
12 13 </button>
13 14 <input class="quantity__input" type="number" name="{{ input_name }}" id="{{ input_id }}" value="{{ value | default: 1 }}" min="{% if line %}0{% else %}1{% endif %}" data-quantity-input>
14 15 <button class="quantity__button" name="plus" type="button" data-quantity-plus>
15 16 {%- render 'icon', icon: 'plus', size: 12 -%}
17 Added <span class="visually-hidden">{{ 'products.product.quantity.increase' | t: product: product_title }}</span>
16 18 </button></div>
links Example data

sections/footer.liquid Changes to sections/footer.liquid: 1 added, 0 removed

Changes to sections/footer.liquid: 1 added, 0 removed
Line before Line after Change Code
37 37 {%- for social in shop.social %}
38 38 <a href="{{ social.url }}" class="list-social__link link">
39 39 {%- render 'icon', icon: social.icon, size: 20 -%}
40 Added <span class="visually-hidden">{{ 'general.social.links.' | append: social.icon | t }}</span>
40 41 </a>
41 42 {%- endfor %}
42 43 </ul>

sections/header.liquid Changes to sections/header.liquid: 3 added, 1 removed

Changes to sections/header.liquid: 3 added, 1 removed
Line before Line after Change Code
22 22 </details>
23 23
24 24 {% if logo_is_heading %}<h1 class="header__heading">{% else %}<div class="header__heading">{% endif %}
25 Removed <a href="{{ routes.root_url }}" class="header__heading-link"><img src="{{ 'logo.svg' | asset_url }}" width="{{ settings.logo_width }}" height="32" class="header__heading-logo"></a>
25 Added <a href="{{ routes.root_url }}" class="header__heading-link"><img src="{{ 'logo.svg' | asset_url }}" width="{{ settings.logo_width }}" height="32" class="header__heading-logo" alt="{{ shop.name | escape }}"></a>
26 26 {% if logo_is_heading %}</h1>{% else %}</div>{% endif %}
27 27
28 28 <nav class="header__inline-menu">
50 50 {%- endif %}
51 51 <a href="{{ routes.account_login_url }}" class="header__icon header__icon--account link">
52 52 {%- render 'icon', icon: 'account', size: icon_size -%}
53 Added <span class="visually-hidden">{{ 'sections.header.account' | t }}</span>
53 54 </a>
54 55 <a href="{{ routes.cart_url }}" class="header__icon header__icon--cart link" id="cart-icon-bubble">
55 56 {%- render 'icon', icon: 'cart', size: icon_size -%}
57 58 <span class="cart-count-bubble" aria-hidden="true"><span data-cart-count>{{ cart.item_count }}</span></span>
58 59 {%- endif -%}
59 60
61 Added <span class="visually-hidden">{{ 'sections.header.cart' | t }}</span>
60 62 </a>
61 63 </div>
62 64 </header>

sections/main-cart.liquid Changes to sections/main-cart.liquid: 1 added, 0 removed

Changes to sections/main-cart.liquid: 1 added, 0 removed
Line before Line after Change Code
26 26 {% render 'quantity-input', input_id: qty_id, input_name: 'updates[]', value: item.quantity, product_title: item.product.title, line: line %}
27 27 <a href="{{ routes.cart_change_url }}?line={{ line }}&amp;quantity=0" class="cart-item__remove cart-item__remove--compact" data-cart-remove>
28 28 {%- render 'icon', icon: 'trash', size: 16 -%}
29 Added <span class="visually-hidden">{{ 'sections.cart.remove' | t: title: item.product.title }}</span>
29 30 </a>
30 31 </div>
31 32 <div class="cart-item__totals">

Shopware: recipe decorative-image-alt

Show the before and after (1)
marks a seeded phone icon inside the hotline link decorative; image-alt stops firing there Example data

Resources/views/storefront/layout/footer/footer.html.twig Changes to Resources/views/storefront/layout/footer/footer.html.twig: 1 added, 1 removed

Changes to Resources/views/storefront/layout/footer/footer.html.twig: 1 added, 1 removed
Line before Line after Change Code
9 9 <div class="footer-column-content">
10 10 <p class="footer-contact-hotline">
11 11 {{ 'footer.serviceHotlineText'|trans }}
12 Removed <a class="footer-contact-hotline-link" href="tel:+49405550192"><img src="{{ asset('bundles/storefront/assets/icon/default/headset.svg') }}" class="footer-contact-hotline-icon" width="16" height="16"> {{ shop.company.phone }}</a>
12 Added <a class="footer-contact-hotline-link" href="tel:+49405550192"><img src="{{ asset('bundles/storefront/assets/icon/default/headset.svg') }}" class="footer-contact-hotline-icon" width="16" height="16" alt=""> {{ shop.company.phone }}</a>
13 13 </p>
14 14 <p class="footer-contact-hours">{{ 'footer.serviceHours'|trans }}</p>
15 15 <p class="footer-contact-form">

WooCommerce: recipe decorative-image-alt

Show the before and after (1)
hides the logo mark inside the home link that has the shop name Example data

header.php Changes to header.php: 1 added, 1 removed

Changes to header.php: 1 added, 1 removed
Line before Line after Change Code
39 39 <div class="site-branding">
40 40 <a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="custom-logo-link" rel="home">
41 41 <?php // VALIDISCAN-SEED svg-img-alt: the logo mark is role="img" without a title or aria-label. ?>
42 Removed <svg class="kb-logo-mark" role="img" width="40" height="40" viewBox="0 0 40 40"><path d="M3 30c6-11 15-17 29-18-7 3-10 8-11 13 6-4 11-5 17-4-9 2-15 8-19 15L3 30Z" fill="#1d3b53"/></svg>
42 Added <svg class="kb-logo-mark" width="40" height="40" viewBox="0 0 40 40" aria-hidden="true" focusable="false"><path d="M3 30c6-11 15-17 29-18-7 3-10 8-11 13 6-4 11-5 17-4-9 2-15 8-19 15L3 30Z" fill="#1d3b53"/></svg>
43 43 <span class="kb-logo-text"><?php bloginfo( 'name' ); ?></span>
44 44 </a>
45 45 </div>

Magento: recipe decorative-image-alt

Show the before and after (1)
marks the promo tile image decorative and validates Example data

app/design/frontend/Validiscan/fixture/Magento_Cms/content/home.html Changes to app/design/frontend/Validiscan/fixture/Magento_Cms/content/home.html: 1 added, 1 removed

Changes to app/design/frontend/Validiscan/fixture/Magento_Cms/content/home.html: 1 added, 1 removed
Line before Line after Change Code
111 111 <div class="block-promo-wrapper block-promo-hp">
112 112 <!-- VALIDISCAN-SEEDED image-alt: promo image without an alt attribute -->
113 113 <a class="block-promo home-main" href="/magento/women/tops-women/jackets-women.html">
114 Removed <img src="/magento/media/wysiwyg/home/home-main.svg" width="400" height="240" />
114 Added <img src="/magento/media/wysiwyg/home/home-main.svg" width="400" height="240" alt="" />
115 115 <span class="content"
116 116 ><!-- VALIDISCAN-SEEDED color-contrast: .info text is #929292 on white (3.1:1) --><span
117 117 class="info"

Start with a free check of your shop.

Send your shop’s address. Validiscan scans it and replies with every failure the scan can prove, with the page and the code.

Request a free check

Or write to info@validiscan.com.

What the free check covers, and what it leaves out
You send
your shop’s address
Pages scanned
up to 15
You get
every failure the scan can prove, with the page and the code
On your shop
public pages only; no login, nothing bought, no forms sent
What it is not
a full audit

Write to Validiscan

For a free check, give the address of your shop and its platform.

Validiscan uses what you send to answer you. Privacy notice