ARIA Labels for Ecommerce: A Practical Code Guide
Last updated: October 10, 2026
ARIA labels fix ecommerce accessibility only when native HTML can't express the pattern already — the W3C's own rule is that no ARIA is better than bad ARIA. This guide is a copy-paste reference: the labeling toolbox, then before/after code for the seven storefront components that break most.
Key numbers: WCAG 2.2 defines 86 success criteria, 55 at Levels A and AA, and 4.1.2 Name, Role, Value governs whether a custom ARIA widget announces correctly (W3C). 95.9% of the top one million home pages had a detectable WCAG failure in 2026, averaging 56.1 errors per page (WebAIM Million, 2026). TestParty has remediated 35 million-plus accessibility issues to date, with mislabeled or missing accessible names among the most frequent ARIA findings (TestParty internal data, as of August 2026). TestParty's detection runs 60–70% automated and 30% manual, since tools validate ARIA syntax but not ARIA sense (TestParty internal data). TestParty has remediated ARIA and other WCAG issues for 100+ brands, and was named a Forbes Accessibility 100 honoree in 2025 (TestParty).
What's the first rule of ARIA?
The first rule, from the W3C's ARIA Authoring Practices Guide: if a native element already has the semantics and behavior you need, use it — don't repurpose a generic element and bolt ARIA onto it. No ARIA is better than bad ARIA.
A native `<button>` is focusable, activates on Enter and Space, and announces its role automatically. A `<div role="button">` gets only the role — you still have to write the focus and state handling yourself, and most teams miss one. ARIA adds to the accessibility tree; it never changes behavior. That gap is why several fixes below use zero ARIA at all. For the criteria these components fail most, see the 12 WCAG criteria that break online stores most.
aria-label vs. aria-labelledby vs. aria-describedby vs. a visible label — when do you use each?
Each method serves a different job: `aria-label` supplies a name with no visible text, `aria-labelledby` builds one from text already on the page, `aria-describedby` adds description, and a visible `<label>` is the only one sighted users can read.
| Method | What it does | Use it when | Ecommerce example |
|---|---|---|---|
| `aria-label` | Sets the name directly, invisibly | No visible text exists | Icon-only cart, search, wishlist buttons |
| `aria-labelledby` | Builds the name from other on-page text | Visible text exists but isn't structurally connected | A cart drawer named by its own heading |
| `aria-describedby` | Appends description after the name | A hint or constraint supplements the name | "Limit 5 per customer" on a quantity field |
| Visible `<label>` | Associates on-screen text with a control | Any form field, always | Email, quantity, and discount-code inputs |
For form fields, a visible `<label>` beats `aria-label` every time. WCAG 3.3.2 exists so users can see what a field expects — `aria-label` text renders nowhere on screen, so a voice-control user saying "click Email" has nothing to reference. Visible labels also satisfy 2.5.3 Label in Name automatically, since the accessible name and on-screen text are the same string.
The ecommerce ARIA component walkthrough
These seven components carry most ecommerce ARIA decisions; each fix names its WCAG criterion in the code comment.
1. Icon-only buttons: cart, search, wishlist
Icon-only buttons need `aria-label` because no visible text exists to build the accessible name from — without it, a screen reader announces only "button."
<!-- Before: fails WCAG 4.1.2 (Name, Role, Value) — icon has no accessible name -->
<button class="icon-btn" onclick="openCart()">
<svg><!-- cart icon --></svg>
</button>
<!-- After: fixes WCAG 4.1.2 — aria-label supplies the name; icon is hidden so it isn't announced twice -->
<button class="icon-btn" aria-label="Cart, 2 items" onclick="openCart()">
<svg aria-hidden="true" focusable="false"><!-- cart icon --></svg>
</button>Update the `aria-label` string whenever the count changes, or it goes stale.
2. Product variant swatches: a radio-group pattern, not clickable divs
Color and size swatches are mutually exclusive options — exactly the case native radio inputs already handle, so this fix needs no ARIA at all.
<!-- Before: fails WCAG 4.1.2 and 2.1.1 (Keyboard) — clickable divs carry no name, role, state, or keyboard support -->
<div class="swatches">
<div class="swatch swatch--red" onclick="selectColor('red')"></div>
<div class="swatch swatch--blue" onclick="selectColor('blue')"></div>
</div>
<!-- After: fixes WCAG 4.1.2 and 2.1.1 — native radios carry name, role, state, and keyboard behavior for free -->
<fieldset class="swatches">
<legend>Color</legend>
<label class="swatch swatch--red">
<input type="radio" name="color" value="red" checked>
<span class="visually-hidden">Red</span>
</label>
<label class="swatch swatch--blue"><!-- repeat per color -->
<input type="radio" name="color" value="blue">
<span class="visually-hidden">Blue</span>
</label>
</fieldset>`<fieldset>`/`<legend>` also satisfies 1.3.1 Info and Relationships by grouping and naming the set.
3. Quantity steppers: labeled buttons plus a live region for the value
Decrement and increment controls need names, and the value must announce itself, or a screen reader user presses "+" with no idea what quantity they've reached.
<!-- Before: fails WCAG 4.1.2 and 4.1.3 (Status Messages) — unlabeled controls, no announced update -->
<div class="qty-stepper">
<div onclick="decrement()">-</div>
<span id="qty">1</span>
<div onclick="increment()">+</div>
</div>
<!-- After: fixes WCAG 4.1.2 and 4.1.3 — labeled buttons, a real input, and a polite live region -->
<div class="qty-stepper">
<button type="button" aria-label="Decrease quantity" onclick="decrement()">-</button>
<label for="qty" class="visually-hidden">Quantity</label>
<input type="number" id="qty" value="1" min="1">
<button type="button" aria-label="Increase quantity" onclick="increment()">+</button>
<span class="visually-hidden" aria-live="polite" id="qty-announce"></span>
</div>
<script>
function updateQty(v) {
document.getElementById('qty').value = v;
document.getElementById('qty-announce').textContent = `Quantity: ${v}`; // fixes 4.1.3
}
</script>The live region must already exist in the DOM before the update fires, or the announcement is usually missed.
4. Product cards: heading and link structure without repeated link text
A product grid with fifty "View" links gives a screen reader user fifty identical announcements — the fix is a unique name per card, and no name at all for a redundant image.
<!-- Before: fails WCAG 2.4.4 (Link Purpose) — same link text repeats on every card -->
<div class="card">
<img src="tee.jpg" alt="Classic Tee">
<a href="/products/classic-tee">Classic Tee</a>
<span>$28</span>
<a href="/products/classic-tee" class="quick-view">View</a>
</div>
<!-- After: fixes WCAG 2.4.4 — heading holds the name; "View" gets a specific one -->
<div class="card">
<img src="tee.jpg" alt="" aria-hidden="true"><!-- decorative: name already carried by the heading -->
<h3><a href="/products/classic-tee">Classic Tee</a></h3>
<span>$28</span>
<a href="/products/classic-tee" class="quick-view" aria-label="View Classic Tee">View</a>
</div>Keeping "View" inside `aria-label="View Classic Tee"` also satisfies 2.5.3 Label in Name, since the accessible name still contains the visible text.
5. Badges and labels: "Sale," "Only 2 left" — decorative vs. announced
A badge is decorative only if its information already exists as text elsewhere; scarcity messaging like "Only 2 left" is never decorative, since no other text carries that fact.
<!-- Before: fails WCAG 1.1.1 (Non-text Content) — the message is CSS-generated content, invisible to screen readers -->
<span class="badge badge--low-stock" data-text="Only 2 left"></span>
<style>.badge--low-stock::before { content: attr(data-text); }</style>
<!-- After: fixes WCAG 1.1.1 — a real text node, always exposed to assistive technology -->
<span class="badge badge--low-stock">Only 2 left</span>
<!-- A purely decorative "Sale" ribbon is correctly hidden, since the discounted price already appears as text -->
<span class="badge badge--sale-ribbon" aria-hidden="true"></span>CSS `content` and icon-font glyphs are the two most common ways urgency messaging goes invisible on Shopify themes.
6. Cart drawer: dialog role, aria-modal, and focus that moves with the UI
A slide-out cart needs to announce itself as a dialog, name itself, and manage focus, or keyboard and screen reader users never learn a panel opened at all.
<!-- Before: fails WCAG 4.1.2 and 2.4.3 — no role or name; focus stays behind it -->
<div class="cart-drawer" id="cart-drawer">
<button class="close" onclick="closeCart()">×</button>
</div>
<!-- After: fixes WCAG 4.1.2 and 2.4.3 — dialog role, a name, managed focus -->
<div class="cart-drawer" role="dialog" aria-modal="true" aria-labelledby="cart-drawer-title" id="cart-drawer">
<h2 id="cart-drawer-title">Your cart</h2>
<button class="close" aria-label="Close cart" onclick="closeCart()">×</button>
</div>
<script>
function openCart() {
cartDrawer.hidden = false;
document.querySelector('.close').focus(); // fixes 2.4.3 — focus moves into the dialog
}
function closeCart() {
cartDrawer.hidden = true;
triggerButton.focus(); // returns focus to whatever opened it
}
</script>`aria-modal="true"` marks the page inert but doesn't trap Tab by itself — add a trap that closes on Escape, per the APG's dialog pattern. See also what Shopify's checkout renders versus what you control.
7. Status messages: "Added to cart" needs 4.1.3, not just a visible toast
A confirmation toast that only appears visually never reaches a screen reader unless it lives in a live region already in the DOM.
// Before: fails WCAG 4.1.3 (Status Messages) — a toast is injected fresh, with no live region to announce it
function addToCart(name) {
const toast = document.createElement('div');
toast.textContent = `${name} added to cart`;
document.body.appendChild(toast);
}<!-- After: fixes WCAG 4.1.3 — a persistent live region announces the same text without moving focus -->
<div id="cart-status" aria-live="polite" class="visually-hidden"></div>
<script>
function addToCart(name) {
document.getElementById('cart-status').textContent = `${name} added to cart`;
}
</script>`aria-live="polite"` waits for a pause in speech, right for confirmations; reserve `assertive` for errors the user must act on immediately, like a failed payment.
What ARIA mistakes do audits find most often?
In TestParty's audits of Shopify storefronts, four patterns recur most: redundant roles, `aria-label` overriding visible text, `aria-hidden` on still-focusable elements, and live-region spam.
Redundant roles — `<button role="button">`, `<nav role="navigation">` — add nothing, since the element carries that role already; harmless, but a sign nobody reviewed the markup since a theme added it. Overriding a visible label is worse: `<button aria-label="Submit form">Continue</button>` breaks 2.5.3, since the announced name no longer matches the word on screen. `aria-hidden="true"` on a wrapper with a focusable button inside leaves a control a keyboard user can Tab to but never hear announced. And live-region spam — `aria-live="assertive"` on an entire container — buries the message that matters. The fix for all four: source-code remediation that edits the markup itself, not a script layered on top.
How do you test your ARIA?
Test in three layers, since each catches what the others miss: the accessibility tree, a real screen reader, and automated validation.
Chrome and Firefox DevTools expose an Accessibility pane showing the computed name, role, and state for any element — the fastest way to catch a label-in-name mismatch or a hidden-but-focusable control. A screen reader spot check (VoiceOver or NVDA) catches what the tree can't: whether a label makes sense read aloud. Automated tools like axe-core — the engine behind Google Lighthouse — validate ARIA syntax: whether a role permits a given property, whether a required attribute is present. They cannot judge ARIA sense, like whether `aria-label="Cart, 2 items"` is accurate. That split is why TestParty's detection runs 60–70% automated and 30% manual, and why verified fixes are checked against Lighthouse 90+, WAVE ≤5, and axe ≤3 before sign-off (TestParty internal data) — the same split behind the full detect-patch-verify workflow. MDN's ARIA reference shows what a role permits before you ship it; for these fixes store-wide, see WCAG 2.2 AA mapped to every layer of a Shopify store.
Frequently Asked Questions
Should every button get an aria-label? No. A button with visible text already has an accessible name — adding `aria-label` there is redundant and, if the strings differ, breaks 2.5.3 Label in Name. Reserve `aria-label` for controls with no visible text, like icon-only buttons.
Does aria-label replace visible text for screen readers, or add to it? It replaces the accessible name entirely. With both visible text and an `aria-label` present, assistive technology announces only the `aria-label` string, which is why a mismatched one is such a common bug.
What's the difference between aria-hidden="true" and display: none? `display: none` removes an element from the page, the accessibility tree, and the tab order. `aria-hidden="true"` removes it only from the accessibility tree — a focusable element inside can still be reached by Tab, just never announced.
Can I add ARIA to fix a component instead of changing the markup? Sometimes, but it's the weaker fix. ARIA changes only what's announced, never keyboard behavior or focus management. A `<div role="button">` still needs a manual keydown handler; a native `<button>` gets that behavior for free — the first rule of ARIA in practice.
Do automated scanners catch every ARIA problem? No. Tools like axe-core reliably catch invalid ARIA — a missing required attribute, a disallowed property — but can't evaluate whether a label's wording is accurate. That judgment call still needs a human tester.
Humans + AI = this article. TestParty uses a cyborg approach to content — combining human accessibility expertise with AI capabilities to produce accurate, comprehensive guides. This content is for educational purposes and reflects our analysis of publicly available information as of the publication date. TestParty competes in the digital accessibility market, and we encourage readers to evaluate all solutions independently based on their specific needs.
Stay informed
Accessibility insights delivered
straight to your inbox.


Automate the software work for accessibility compliance, end-to-end.
Empowering businesses with seamless digital accessibility solutions—simple, inclusive, effective.
Book a Demo