Common accessibility issues in WooCommerce stores and how to fix them
WooCommerce accessibility problems come from two places: the underlying WordPress theme and the WooCommerce component layer on top of it. Most store owners inherit both without auditing either. When we scan WooCommerce stores, we see the same failure patterns across stores that otherwise look completely different, because the core WooCommerce templates and the most popular themes share the same component patterns. This guide covers the failures we measure most often and shows what the fix looks like at the code level.
Why WooCommerce stores have accessibility issues by default
WooCommerce's own templates have improved steadily, and the WooCommerce Blocks introduced since version 8.x bring better semantic structure to the cart and checkout. But most live stores are still running the classic shortcode-based product pages, which ship with older widget patterns. On top of that, themes and third-party plugins add their own interactive components, each with their own accessibility history.
The Storefront theme, WooCommerce's official starter theme, is a reasonable baseline but has gaps. More design-forward themes like Divi, Astra, and OceanWP add visual polish with custom JavaScript widgets that were tested in a browser but not with a keyboard or screen reader. The accessibility of your WooCommerce store is roughly the product of: core WooCommerce templates, your active theme, your active plugins, and any customizations added to functions.php or a child theme.
What follows are the issues that come up most consistently when we scan real WooCommerce stores.
1. Unlabeled product variation dropdowns
Variable products in WooCommerce let customers choose size, color, and other attributes before adding to cart. Each attribute becomes a <select> element. The label for that select is often missing, hidden in a way that breaks screen reader association, or styled as a heading that is visually nearby but not programmatically linked.
The failure shows as a select-name violation in axe-core: a form element with no accessible label. Screen readers announce this as "edit text" or "combo box" with no description of what the shopper is selecting.
What causes it
The default WooCommerce variation template outputs a <label> above the <select>, but some themes override woocommerce/templates/single-product/add-to-cart/variation.php and drop the label, replacing it with a visually styled heading that has no for attribute pointing to the select's id.
The fix
Keep or restore the label/for relationship. If you are overriding the variation template in your child theme, make sure the label element is present and its for attribute matches the select's id:
<!-- each attribute's select in single-product/add-to-cart/variation.php -->
<label for="pa_color">Color</label>
<select id="pa_color" name="attribute_pa_color">
<option value="">Choose an option</option>
<option value="black">Black</option>
<option value="white">White</option>
</select>
WooCommerce's own variation template does this correctly by default. If your theme has removed the label, restore it in your child theme override of that template file.
2. Quantity input with no label
The quantity field on product pages and in the cart is an <input type="number">. In many theme implementations, this input has a visible +/- button interface but the underlying input has no accessible name -- no <label>, no aria-label, and no aria-labelledby. A screen reader user cannot tell what the field is for or which product the quantity applies to.
In the cart, this is worse: multiple rows each have a quantity input, and if none of them have labels, a screen reader user tabbing through the cart hears "spin button" repeated for every row with no product association.
The fix
WooCommerce's default quantity template at woocommerce/templates/global/quantity-input.php includes a screen-reader label. If your theme overrides this template, verify the label is present. For cart rows specifically, the label should reference the product name:
<!-- in your quantity-input.php override -->
<label class="screen-reader-text" for="quantity_<?php echo esc_attr( $product_id ); ?>">
<?php printf( esc_html__( 'Quantity for %s', 'woocommerce' ), $product_name ); ?>
</label>
<input
type="number"
id="quantity_<?php echo esc_attr( $product_id ); ?>"
name="quantity"
value="1"
min="1"
aria-label="<?php printf( esc_html__( 'Quantity for %s', 'woocommerce' ), $product_name ); ?>"
>
The screen-reader-text class hides the label visually while keeping it in the accessibility tree. The aria-label is a redundant but reliable fallback for assistive technologies that sometimes skip visually-hidden labels.
3. Product tabs with broken ARIA structure
WooCommerce product pages show a tab interface below the main product content: Description, Additional Information, and Reviews (or a custom set depending on plugins). These tabs should use ARIA tab roles so screen readers can identify them as a tab group, announce the selected tab, and let keyboard users navigate with the arrow keys.
Most theme implementations style the tabs correctly but implement them as a simple list of anchor links pointing to div IDs, not as a proper ARIA tablist. The result is that screen readers treat each tab as a heading-level link, not as a selectable tab in a group.
What the pattern looks like
<!-- broken: anchor links styled to look like tabs -->
<ul class="tabs wc-tabs">
<li class="description_tab active"><a href="#tab-description">Description</a></li>
<li class="reviews_tab"><a href="#tab-reviews">Reviews (4)</a></li>
</ul>
<div id="tab-description">...</div>
<div id="tab-reviews">...</div>
The fix
Override WooCommerce's tab template or add a JavaScript enhancement that applies ARIA roles after the page loads. If using the JS approach (less likely to break on WooCommerce updates):
// in your child theme's JS file, run after DOMContentLoaded
const tabList = document.querySelector('ul.wc-tabs');
if (tabList) {
tabList.setAttribute('role', 'tablist');
tabList.querySelectorAll('li').forEach((li, i) => {
const link = li.querySelector('a');
const panelId = link.getAttribute('href').replace('#', '');
const panel = document.getElementById(panelId);
const tabId = 'tab-' + panelId;
link.setAttribute('role', 'tab');
link.setAttribute('id', tabId);
link.setAttribute('aria-controls', panelId);
link.setAttribute('aria-selected', li.classList.contains('active') ? 'true' : 'false');
link.setAttribute('tabindex', li.classList.contains('active') ? '0' : '-1');
if (panel) {
panel.setAttribute('role', 'tabpanel');
panel.setAttribute('aria-labelledby', tabId);
panel.setAttribute('tabindex', '0');
}
});
// arrow key navigation between tabs
tabList.addEventListener('keydown', (e) => {
const tabs = [...tabList.querySelectorAll('[role="tab"]')];
const idx = tabs.indexOf(document.activeElement);
if (e.key === 'ArrowRight' && idx < tabs.length - 1) {
tabs[idx + 1].focus();
} else if (e.key === 'ArrowLeft' && idx > 0) {
tabs[idx - 1].focus();
}
});
}
4. Add to Cart buttons on archive pages that do not name the product
On shop archive pages (the main shop, category pages, tag pages), WooCommerce outputs an "Add to cart" button for each product. The button text reads "Add to cart" for every product on the page. To a sighted user, context makes it clear which product each button belongs to because the product name appears above it. To a screen reader user navigating by button, all they hear is "Add to cart, Add to cart, Add to cart" repeated for every product.
This is a button-name violation: each button has an accessible name, but the name does not describe what it adds to the cart. It is technically a WCAG 2.4.6 (Headings and Labels) and 1.3.1 (Info and Relationships) concern. It becomes practically significant as stores grow the number of products per page.
The fix
Use a filter to add an aria-label to each archive page Add to Cart button that includes the product name:
// in functions.php or a plugin
add_filter( 'woocommerce_loop_add_to_cart_args', function( $args, $product ) {
$args['attributes']['aria-label'] = sprintf(
/* translators: %s: product name */
__( 'Add %s to cart', 'woocommerce' ),
wp_strip_all_tags( $product->get_name() )
);
return $args;
}, 10, 2 );
This adds aria-label="Add Vintage Leather Wallet to cart" to each button, giving screen reader users complete context without changing the visible text.
5. Star rating inputs in the review form
WooCommerce's product review form includes a star rating widget. The underlying input is a set of radio buttons, but many theme implementations hide them with CSS that also removes them from the accessibility tree. The visual stars become clickable for sighted users but invisible to keyboard and screen reader users. The review form becomes impossible to submit with a rating via keyboard alone.
What causes it
Some themes use display: none or visibility: hidden on the radio inputs rather than the screen-reader-text pattern (position absolute, clip, etc.). display: none removes the element from the accessibility tree entirely, so there is no input to focus or activate.
The fix
Replace display: none with visually-hidden CSS on the radio inputs. In your child theme's style.css, override the theme's hiding rule:
/* Replace any theme rule that uses display:none or visibility:hidden on star inputs */
.woocommerce-product-rating .stars input,
#review_form .stars input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
This pattern (commonly called .sr-only or .screen-reader-text) keeps the element in the accessibility tree and reachable by keyboard while hiding it visually. The visual star labels can still receive click events.
6. Mini cart and cart drawer focus management
Many WooCommerce themes implement a slide-out cart drawer triggered by clicking the cart icon in the header. When the drawer opens, focus stays on the cart icon that opened it -- it does not move inside the drawer. Keyboard users cannot reach the cart contents. Screen reader users have no notification that the drawer opened. When the drawer closes, focus often goes to the document body rather than back to the trigger.
This is a focus management failure: any component that adds or reveals significant content should move focus into that content when opened, trap focus within it while it is open, and return focus to the trigger when it closes.
The fix
If you are using a theme with a custom cart drawer, add focus management in your child theme's JavaScript. The key behaviors:
// focus management for a cart drawer
function openCartDrawer(drawer, trigger) {
drawer.removeAttribute('hidden');
drawer.setAttribute('aria-hidden', 'false');
// move focus to first focusable element inside the drawer
const firstFocusable = drawer.querySelector(
'a[href], button:not([disabled]), input, [tabindex]:not([tabindex="-1"])'
);
if (firstFocusable) firstFocusable.focus();
// trap focus within the drawer while it is open
drawer.addEventListener('keydown', trapFocus);
// close on Escape
drawer.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeCartDrawer(drawer, trigger);
});
}
function closeCartDrawer(drawer, trigger) {
drawer.setAttribute('hidden', '');
drawer.setAttribute('aria-hidden', 'true');
drawer.removeEventListener('keydown', trapFocus);
// return focus to the element that opened the drawer
if (trigger) trigger.focus();
}
function trapFocus(e) {
const focusable = [...this.querySelectorAll(
'a[href], button:not([disabled]), input, [tabindex]:not([tabindex="-1"])'
)];
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault(); last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault(); first.focus();
}
}
}
The drawer should also carry role="dialog" and aria-label="Shopping cart" so screen readers announce it as a dialog when focus moves into it.
7. Wishlist plugin buttons with no accessible name
YITH WooCommerce Wishlist, TI WooCommerce Wishlist, and similar plugins add a heart or bookmark icon to product cards that lets shoppers save items. These buttons are almost always icon-only: a visual icon with no text, and often no aria-label either. The result is a button the screen reader announces as "button" or reads the icon's filename.
The same issue applies to quick-view buttons, compare buttons, and social sharing icons that plugins add to the product loop.
The fix
Most wishlist plugins expose a filter or template override for the button. For YITH Wishlist, you can add the aria-label via their template override or the yith_wcwl_add_to_wishlistbutton_classes filter. The simplest option that works regardless of plugin is a JavaScript pass on page load:
// add accessible names to icon-only wishlist and compare buttons
document.querySelectorAll('.yith-wcwl-add-button a, .add_to_wishlist').forEach((btn) => {
if (!btn.getAttribute('aria-label') && !btn.textContent.trim()) {
const product = btn.closest('.product');
const name = product
? product.querySelector('.woocommerce-loop-product__title')?.textContent.trim()
: null;
btn.setAttribute('aria-label', name ? 'Add ' + name + ' to wishlist' : 'Add to wishlist');
}
});
This approach survives plugin updates because it runs at runtime rather than overriding plugin templates. The product name lookup makes the label specific enough to be useful when multiple products appear on the same page.
Where to start
If you find all of these issues on your store at once, prioritize by where customers are most likely to abandon. The variation dropdown and quantity input problems are on the product page -- a conversion-critical path. The archive page button labeling problem affects every category and shop page. The cart drawer focus issue blocks keyboard users from completing checkout after adding items. Fix those three first before the review form and wishlist buttons.
Automated scanning catches these specific patterns reliably. A free scan of your WooCommerce store will show which of these issues are present, how many instances, and on which pages, so you can hand a specific task list to your developer rather than a vague "make it accessible" request.
Run a free scan on your store and see exactly what your developer needs to fix.