Accessibility audit · WCAG 2.2 Level A/AA · 2026-11-06
Sample apparel store (anonymised)
Pages tested: 3 · desktop (1366px) and mobile (375px) · axe-core 4.10.2 plus scripted keyboard, structure and reflow checks, reviewed and written up by an AI agent.
1. Fix these first
| # | Issue | Severity | Elements | Pages |
|---|---|---|---|---|
| 1 | Buttons must have discernible text | critical | 237 | 3/3 |
| 2 | Elements must only use permitted ARIA attributes | serious | 120 | 3/3 |
| 3 | Links must have discernible text | serious | 110 | 3/3 |
| 4 | Elements must meet minimum color contrast ratio thresholds | serious | 50 | 3/3 |
| 5 | All touch targets must be 24px large, or leave sufficient space | serious | 26 | 3/3 |
| 6 | Interactive controls must not be nested | serious | 18 | 3/3 |
| 7 | Images must have alternative text | critical | 6 | 3/3 |
| 8 | Form elements must have labels | critical | 6 | 3/3 |
| 9 | [role="img"] elements must have an alternative text | serious | 9 | 3/3 |
| 10 | Scrollable region must have keyboard access | serious | 8 | 3/3 |
2. Issues in detail
1. Buttons must have discernible text critical
Rule button-name · WCAG 4.1.2 Name, Role, Value (A) · 237 elements on 3 page(s) · desktop + mobile
Who it affects: Screen-reader users hear only "button" and cannot tell what it does.
Where (examples):
https://example-store.com/ (desktop) → ._searchDesktop_vooo2_184 > ._inputWrapper_g4ym7_8 > ._form_g4ym7_1[data-cnstrc-search-form="true"][action="#"] > ._searchIcon_g4ym7_15[data-cnstrc-search-submit-btn="true"]
<button class="_searchIcon_g4ym7_15" data-cnstrc-search-submit-btn="true">
https://example-store.com/ (desktop) → ._search_mncfk_48
<button class="brand-module_button__pMTrr brand-module_minimal__eSLFC brand-module_sm__jM2Lm _search_mncfk_48">
https://example-store.com/ (desktop) → ._close_mncfk_42
<button class="brand-module_button__pMTrr brand-module_minimal-subdued__Og-5C brand-module_sm__jM2Lm _close_mncfk_42">
https://example-store.com/ (desktop) → div[data-visible="true"][data-index="0"][data-active-slide="true"] > ._productCard_1dmli_1._skeleton_1dmli_76._customSwiperSlide_10jlh_122 > ._productCardContent_bumbi_1._skeleton_bumbi_74 > ._footer_bumbi_46._noGap_bumbi_51 > ._priceAndWishlist_bumbi_54 > .product-card-wishlist-btn.brand-module_minimal__eSLFC[data-cnstrc-btn="add_to_wishlist"]
<button class="brand-module_button__pMTrr brand-module_minimal__eSLFC brand-module_sm__jM2Lm product-card-wishlist-btn" style="--wishlist-color:#000000;pointer-events:none" disabled="" data-cnstrc-btn="add_to_wishlist">
Shopify: icon-only header buttons (e.g. snippets/header-*.liquid or sections/header.liquid)
<!-- before -->
<button class="header__icon header__icon--search">{% render 'icon-search' %}</button>
<!-- after: a visually hidden text label -->
<button class="header__icon header__icon--search" type="button">
{% render 'icon-search' %}
<span class="visually-hidden">{{ 'general.search.search' | t }}</span>
</button>
/* add once to your main CSS if your theme lacks it */
.visually-hidden{position:absolute!important;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}Carousel arrows / close buttons
<button class="slider-button slider-button--next" aria-label="Next slide">…</button> <button class="drawer__close" aria-label="Close cart">…</button>
Reference: https://dequeuniversity.com/rules/axe/4.10/button-name?application=axeAPI
2. Elements must only use permitted ARIA attributes serious
Rule aria-prohibited-attr · WCAG 4.1.2 (A) · 120 elements on 3 page(s) · desktop + mobile
Who it affects: Labels set with aria-label on elements that cannot carry one are ignored by screen readers.
Where (examples):
https://example-store.com/ (desktop) → div[data-visible="true"][data-index="0"][data-active-slide="true"] > ._productCard_1dmli_1._skeleton_1dmli_76._customSwiperSlide_10jlh_122
<div class="_productCard_1dmli_1 _skeleton_1dmli_76 _customSwiperSlide_10jlh_122" aria-label="Product Card for " style="--background-color:transparent;--background-color-hover:transparent;--backdrop-filter:unset;--skeleton-background-color:#E0E0E0;--skeleton-highlight-color:#F0F0F0">
https://example-store.com/ (desktop) → div[data-visible="true"][data-index="1"][data-active-slide="false"] > ._productCard_1dmli_1._skeleton_1dmli_76._customSwiperSlide_10jlh_122
<div class="_productCard_1dmli_1 _skeleton_1dmli_76 _customSwiperSlide_10jlh_122" aria-label="Product Card for " style="--background-color:transparent;--background-color-hover:transparent;--backdrop-filter:unset;--skeleton-background-color:#E0E0E0;--skeleton-highlight-color:#F0F0F0">
https://example-store.com/ (desktop) → ._mobile_p7ewd_20 > div:nth-child(2) > .brand-module_wrapper__BSQ7a > ._carousel_10jlh_17[gap="16"][pagination="[object Object]"] > .carousel-scroller.brand-module_scroller__G7Z1c > div[data-index="2"][data-active-slide="false"][data-visible="false"] > ._productCard_1dmli_1._skeleton_1dmli_76._customSwiperSlide_10jlh_122
<div class="_productCard_1dmli_1 _skeleton_1dmli_76 _customSwiperSlide_10jlh_122" aria-label="Product Card for " style="--background-color:transparent;--background-color-hover:transparent;--backdrop-filter:unset;--skeleton-background-color:#E0E0E0;--skeleton-highlight-color:#F0F0F0">
https://example-store.com/ (desktop) → ._mobile_p7ewd_20 > div:nth-child(2) > .brand-module_wrapper__BSQ7a > ._carousel_10jlh_17[gap="16"][pagination="[object Object]"] > .carousel-scroller.brand-module_scroller__G7Z1c > div[data-index="3"][data-active-slide="false"][data-visible="false"] > ._productCard_1dmli_1._skeleton_1dmli_76._customSwiperSlide_10jlh_122
<div class="_productCard_1dmli_1 _skeleton_1dmli_76 _customSwiperSlide_10jlh_122" aria-label="Product Card for " style="--background-color:transparent;--background-color-hover:transparent;--backdrop-filter:unset;--skeleton-background-color:#E0E0E0;--skeleton-highlight-color:#F0F0F0">
aria-label on a plain div/span is ignored
<!-- before --> <span class="badge" aria-label="Sale">-20%</span> <!-- after --> <span class="badge"><span class="visually-hidden">Sale: </span>-20%</span>
Reference: https://dequeuniversity.com/rules/axe/4.10/aria-prohibited-attr?application=axeAPI
3. Links must have discernible text serious
Rule link-name · WCAG 2.4.4 Link Purpose / 4.1.2 (A) · 110 elements on 3 page(s) · desktop + mobile
Who it affects: Screen-reader and voice-control users cannot identify or activate the link.
Where (examples):
https://example-store.com/ (desktop) → .swiper-slide-active > ._container_1nl8d_16 > ._overlayWrapper_a9y6m_1 > ._imageContainer_1nl8d_91 > ._image_1nl8d_91 > a[data-discover="true"]
<a href="/collections/new-arrival-styles" data-discover="true">
https://example-store.com/ (desktop) → ._shopByCardLink_15fr9_113[href$="all-casual-shorts"][data-discover="true"]
<a class="_shopByCardLink_15fr9_113" href="/collections/all-casual-shorts" data-discover="true"></a>
https://example-store.com/ (desktop) → ._image_15fr9_66 > a[href$="all-casual-shorts"][data-discover="true"]
<a href="/collections/all-casual-shorts" data-discover="true">
https://example-store.com/ (desktop) → section[data-section-name="HP - Categories 1"] > ._cards_15fr9_27 > ._shopByCard_15fr9_46:nth-child(2) > ._shopByCardLink_15fr9_113[data-discover="true"]
<a class="_shopByCardLink_15fr9_113" href="/collections/mens-collared-shirts" data-discover="true"></a>
Social icon links (often snippets/social-icons.liquid)
<a href="{{ settings.social_instagram_link }}" class="list-social__link">
{% render 'icon-instagram' %}
<span class="visually-hidden">Instagram</span>
</a>Logo / image links
<a href="{{ routes.root_url }}"><img src="…" alt="{{ shop.name }} home"></a>Reference: https://dequeuniversity.com/rules/axe/4.10/link-name?application=axeAPI
4. Elements must meet minimum color contrast ratio thresholds serious
Rule color-contrast · WCAG 1.4.3 Contrast (Minimum) (AA) · 50 elements on 3 page(s) · desktop + mobile
Who it affects: Low-vision users and anyone on a phone in sunlight struggle to read the text.
Where (examples):
https://example-store.com/ (desktop) → ._changeButton_b211o_38 > .brand-module_content__iXfTs
<div class="brand-module_content__iXfTs">CHANGE TO Denmark SITE</div>
https://example-store.com/ (desktop) → ._continueButton_b211o_42
<a class="_continueButton_b211o_42" href="/" data-discover="true">Continue to the United States site</a>
https://example-store.com/ (desktop) → ._link_1mwod_27._navItem_1mwod_1[role="button"]:nth-child(1) > ._firstLevelLink_1mwod_16[data-discover="true"]
<a class="_firstLevelLink_1mwod_16" style="--color:#f24392" href="/collections/featured-collections" data-discover="true">Featured </a>
https://example-store.com/ (desktop) → ._simpleLink_1mwod_30._navItem_1mwod_1[target="_blank"]:nth-child(9)
<a class="_navItem_1mwod_1 _simpleLink_1mwod_30" style="color:#9A9846" href="https://www.cheekies.com/?utm_source=brand-nav&utm_medium=referral&utm_campaign=cheekieslaunch" target="_blank">For Her</a>
Fix the colour token once (Theme settings → Colors, or CSS variables)
:root{
/* example: #9a9a9a on white is 2.8:1 → fails; #6b6b6b is 5.3:1 → passes */
--color-foreground-muted: #6b6b6b;
}
.price__compare, .caption, .announcement-bar__message { color: var(--color-foreground-muted); }Check each colour pair with the WebAIM contrast checker before publishing.
Reference: https://dequeuniversity.com/rules/axe/4.10/color-contrast?application=axeAPI
5. All touch targets must be 24px large, or leave sufficient space serious
Rule target-size · WCAG 2.5.8 Target Size (Minimum) (AA, WCAG 2.2) · 26 elements on 3 page(s) · desktop + mobile
Who it affects: People with tremors or large fingers mis-tap small controls, especially on mobile.
Where (examples):
https://example-store.com/ (desktop) → ._link_1mwod_27._navItem_1mwod_1[role="button"]:nth-child(1)
<div class="_navItem_1mwod_1 _link_1mwod_27" role="button" tabindex="0">
https://example-store.com/ (desktop) → ._link_1mwod_27._navItem_1mwod_1[role="button"]:nth-child(2)
<div class="_navItem_1mwod_1 _link_1mwod_27" role="button" tabindex="0">
https://example-store.com/ (desktop) → ._link_1mwod_27._navItem_1mwod_1[role="button"]:nth-child(3)
<div class="_navItem_1mwod_1 _link_1mwod_27" role="button" tabindex="0">
https://example-store.com/ (desktop) → ._link_1mwod_27._navItem_1mwod_1[role="button"]:nth-child(4)
<div class="_navItem_1mwod_1 _link_1mwod_27" role="button" tabindex="0">
Give small controls a 24×24 px minimum hit area
.swatch, .slider-counter__link, .drawer__close, .quantity__button {
min-width: 24px; min-height: 24px;
}
/* or keep the visual size and enlarge the hit area */
.slider-counter__link { position: relative; }
.slider-counter__link::after { content:""; position:absolute; inset:-6px; }Reference: https://dequeuniversity.com/rules/axe/4.10/target-size?application=axeAPI
6. Interactive controls must not be nested serious
Rule nested-interactive · WCAG 4.1.2 Name, Role, Value (A) · 18 elements on 3 page(s) · desktop
Who it affects: Assistive tech cannot reach or announce a control placed inside another control.
Where (examples):
https://example-store.com/ (desktop) → ._link_1mwod_27._navItem_1mwod_1[role="button"]:nth-child(1)
<div class="_navItem_1mwod_1 _link_1mwod_27" role="button" tabindex="0">
https://example-store.com/ (desktop) → ._link_1mwod_27._navItem_1mwod_1[role="button"]:nth-child(2)
<div class="_navItem_1mwod_1 _link_1mwod_27" role="button" tabindex="0">
https://example-store.com/ (desktop) → ._link_1mwod_27._navItem_1mwod_1[role="button"]:nth-child(3)
<div class="_navItem_1mwod_1 _link_1mwod_27" role="button" tabindex="0">
https://example-store.com/ (desktop) → ._link_1mwod_27._navItem_1mwod_1[role="button"]:nth-child(4)
<div class="_navItem_1mwod_1 _link_1mwod_27" role="button" tabindex="0">
Product card: don't put a button inside a link
<!-- before: <a href="{{ product.url }}"> … <button>Quick add</button> … </a> -->
<div class="card">
<a href="{{ product.url }}" class="card__link">{{ product.title }}</a>
<button type="button" class="quick-add">Quick add <span class="visually-hidden">{{ product.title }}</span></button>
</div>Reference: https://dequeuniversity.com/rules/axe/4.10/nested-interactive?application=axeAPI
7. Images must have alternative text critical
Rule image-alt · WCAG 1.1.1 Non-text Content (A) · 6 elements on 3 page(s) · desktop + mobile
Who it affects: Screen-reader users get no information about the image (often a product photo).
Where (examples):
https://example-store.com/ (desktop) → .brand-module_container__WFekk > img
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAXCAYAAAARIY8tAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAK5SURBVHgB7VJLSFRhFD7nv9d53CFnmnyMMjkWaooPsoJaKNpGyYjalIVEBBFZ4cbQiBaGVBBCi2rRqnIT2aKgSKqFRlERGZSLHpNlPtIxdR7OdXTu/P/pdxs2jlNLv83l/pzzfec73wFYwf+Ey3v6uGvw
Shopify: use the media alt text field
{{ product.featured_media | image_url: width: 800 | image_tag: alt: product.featured_media.alt | default: product.title }}
<!-- decorative images only -->
<img src="…" alt="" role="presentation">Then fill in alt text in Admin → Products → (image) → Add alt text.
Reference: https://dequeuniversity.com/rules/axe/4.10/image-alt?application=axeAPI
8. Form elements must have labels critical
Rule label · WCAG 1.3.1 / 4.1.2 (A) · 6 elements on 3 page(s) · desktop + mobile
Who it affects: Screen-reader users do not know what to type into the field (e.g. newsletter or search).
Where (examples):
https://example-store.com/ (desktop) → ._searchDesktop_vooo2_184 > ._inputWrapper_g4ym7_8 > ._form_g4ym7_1[data-cnstrc-search-form="true"][action="#"] > ._input_g4ym7_8[name="q"][placeholder=""]
<input name="q" class="_input_g4ym7_8" placeholder="" data-cnstrc-search-input="true" value="">
https://example-store.com/ (mobile) → ._searchMobile_vooo2_162 > ._inputWrapper_g4ym7_8 > ._form_g4ym7_1[data-cnstrc-search-form="true"][action="#"] > ._input_g4ym7_8[name="q"][placeholder=""]
<input name="q" class="_input_g4ym7_8" placeholder="" data-cnstrc-search-input="true" value="">
Newsletter / search inputs
<label for="NewsletterForm-{{ section.id }}" class="visually-hidden">Email address</label>
<input id="NewsletterForm-{{ section.id }}" type="email" name="contact[email]" autocomplete="email" placeholder="Email">Reference: https://dequeuniversity.com/rules/axe/4.10/label?application=axeAPI
9. [role="img"] elements must have an alternative text serious
Rule role-img-alt · WCAG 1.1.1 (A) · 9 elements on 3 page(s) · desktop + mobile
Who it affects: Elements marked as images have no text alternative.
Where (examples):
https://example-store.com/ (desktop) → ._lg_yegym_14 > span[role="img"]
<span role="img" style="display:inline-block;font-size:1em;line-height:1em;vertical-align:middle">🇺🇸</span>
https://example-store.com/ (desktop) → ._md_yegym_11 > span[role="img"]
<span role="img" style="display:inline-block;font-size:1em;line-height:1em;vertical-align:middle">🇺🇸</span>
Elements with role=img
<div role="img" aria-label="5 out of 5 stars">★★★★★</div>
Reference: https://dequeuniversity.com/rules/axe/4.10/role-img-alt?application=axeAPI
10. Scrollable region must have keyboard access serious
Rule scrollable-region-focusable · WCAG 2.1.1 Keyboard (A) · 8 elements on 3 page(s) · desktop + mobile
Who it affects: Keyboard users cannot scroll a scrollable area (e.g. a horizontal product slider).
Where (examples):
https://example-store.com/ (desktop) → ._mobile_p7ewd_20 > div:nth-child(2) > .brand-module_wrapper__BSQ7a > ._carousel_10jlh_17[gap="16"][pagination="[object Object]"] > .carousel-scroller.brand-module_scroller__G7Z1c
<div class="carousel-scroller brand-module_scroller__G7Z1c" style="--carousel-gap:8px">
https://example-store.com/cart (desktop) → ._mobile_p7ewd_20 > div:nth-child(2) > .brand-module_wrapper__BSQ7a > div[gap="16"][pagination="[object Object]"][data-fs-recommendations-pod-id="nav-trending-new"] > .carousel-scroller.brand-module_scroller__G7Z1c
<div class="carousel-scroller brand-module_scroller__G7Z1c" style="--carousel-gap: 8px;">
https://example-store.com/cart (desktop) → div[data-fs-recommendations-pod-id="view-cart"] > .carousel-scroller.brand-module_scroller__G7Z1c
<div class="carousel-scroller brand-module_scroller__G7Z1c" style="--carousel-gap: 10px;">
Horizontal sliders
<div class="slider" tabindex="0" role="region" aria-label="Featured products"> … </div>
Reference: https://dequeuniversity.com/rules/axe/4.10/scrollable-region-focusable?application=axeAPI
11. ARIA progressbar nodes must have an accessible name serious
Rule aria-progressbar-name · WCAG 1.1.1 / 4.1.2 (A) · 6 elements on 3 page(s) · desktop + mobile
Who it affects: Progress bars (e.g. free-shipping bar) are announced without meaning.
Where (examples):
https://example-store.com/ (desktop) → .brand-module_progressBar__6Z-Ub
<div class="brand-module_progressBar__6Z-Ub brand-module_md__ksG64" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div class="brand-module_bar__tnsXU" style="--percentage: 0%;"></div></div>
Free-shipping bar
<div role="progressbar" aria-label="Progress toward free shipping" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ percent }}"></div>Reference: https://dequeuniversity.com/rules/axe/4.10/aria-progressbar-name?application=axeAPI
3. Keyboard, structure and reflow checks
| Page | Finding |
|---|---|
| https://example-store.com/ | No visible <h1> heading (1.3.1 / 2.4.6 best practice) |
| https://example-store.com/ | No "skip to content" link found among the first in-page links (2.4.1 Bypass Blocks) |
| https://example-store.com/ | Horizontal scrolling at 320px width: fails (page 344px wide at 320px) (1.4.10 Reflow) |
| https://example-store.com/collections/all | Heading levels skipped: 1→3 at "ORIGINALS STRETCH SHORT" (1.3.1 best practice) |
| https://example-store.com/collections/all | No "skip to content" link found among the first in-page links (2.4.1 Bypass Blocks) |
| https://example-store.com/collections/all | Autoplaying media: video (muted), no controls (2.2.2 Pause, Stop, Hide) |
| https://example-store.com/cart | No visible <h1> heading (1.3.1 / 2.4.6 best practice) |
| https://example-store.com/cart | No "skip to content" link found among the first in-page links (2.4.1 Bypass Blocks) |
4. Pages tested
| Page | Desktop | Mobile | Reflow @320px |
|---|---|---|---|
| https://example-store.com/ | 99 instances / 11 rules | 85 instances / 9 rules | fails (page 344px wide at 320px) |
| https://example-store.com/collections/all | 81 instances / 11 rules | 87 instances / 10 rules | passes |
| https://example-store.com/cart | 130 instances / 11 rules | 114 instances / 9 rules | passes |
5. Next steps
- Fix items 1–5 in section 1. They usually live in a few theme files (header, product card, footer) and fix many elements at once.
- Reply with the date when fixes are live, and we will re-run the same tests once at no charge within 30 days.
- Test checkout and any third-party widgets (reviews, chat, pop-ups) separately; they are often outside your theme.
This audit was produced by an AI agent operated on behalf of the seller. Questions: reply to the delivery email.