Common accessibility issues in BigCommerce stores and how to fix them
BigCommerce stores built on Stencil themes share a predictable set of accessibility patterns, most of which come from theme template choices rather than anything the store owner controls directly. When we scan BigCommerce stores, the same failures appear across different brands because they trace back to the same Cornerstone starter theme components, carried forward into custom themes with modifications that addressed visual requirements but not keyboard or screen reader behavior.
This guide covers the issues that come up most consistently in BigCommerce scans, explains what causes them in the Stencil context, and shows the template and JavaScript changes that fix each one.
How Stencil themes work
BigCommerce's Stencil framework uses Handlebars templating. Product pages, category pages, cart, and checkout are defined in .html files that mix HTML markup with Handlebars expressions ({{variable}}, {{> partials/component}}). Theme JavaScript typically lives in assets/js/theme/ and is organized by page type. SCSS goes in assets/scss/.
To modify a Stencil theme, you download the theme files through the BigCommerce control panel (Storefront, then Themes, then Advanced, then Download Current Theme), edit locally using the Stencil CLI for previewing, and upload the modified theme back. Changes take effect when you publish the theme. BigCommerce's Page Builder (the visual editor) does not expose the underlying Handlebars templates for editing; template-level fixes require downloading and re-uploading the theme.
The Cornerstone theme is the reference implementation that most custom BigCommerce themes are based on. The patterns below reference Cornerstone conventions, but the underlying issues appear in derived themes as well.
1. Product option swatches with no accessible name
Color and size swatch selectors on BigCommerce product pages are one of the most frequent accessibility findings. A product with multiple color options renders a set of buttons, one per option. When each button contains only a color swatch image or a filled element with no text alternative, a screen reader user navigating the variant selector hears "button" repeated for each option, with no way to identify which color or size each button represents. Axe-core flags this as a button-name violation (WCAG 4.1.2).
The Cornerstone pattern looks roughly like this in the Handlebars template:
<!-- templates/components/products/options/swatch.html -->
<li class="form-option">
<input class="form-option-input" type="radio"
name="attribute[{{id}}]" id="attribute-{{sanitize id}}-{{sanitize value}}"
value="{{id}}">
<label class="form-option-label" for="attribute-{{sanitize id}}-{{sanitize value}}">
<span class="form-option-variant form-option-variant--color"
style="background-color:{{data}}">
</span>
<span class="form-option-variant--label">{{label}}</span>
</label>
</li>
In this pattern, the label element is associated with the radio input via for and id, and the {{label}} expression renders the option name as text content. If your theme uses this exact structure, the accessible name is already there and this finding may not apply to your swatch selectors.
The accessibility failure happens when themes replace the radio input pattern with a pure button pattern, or when the label's text span is visually hidden or removed:
<!-- Problematic pattern: button with no text -->
<li class="form-option">
<button type="button" class="swatch-btn" data-value="Navy">
<span class="swatch-fill" style="background-color:#1a2744;"></span>
</button>
</li>
The fix in Handlebars is to include the option label as an aria-label attribute on the button:
<!-- Fixed: accessible name from aria-label -->
<li class="form-option">
<button type="button" class="swatch-btn"
aria-label="Color: {{label}}" data-value="{{label}}">
<span class="swatch-fill" aria-hidden="true"
style="background-color:{{data}}"></span>
</button>
</li>
If your theme renders swatches with JavaScript rather than from a Handlebars template, the same fix applies in the DOM construction code:
// In assets/js/theme/product.js or your theme's option init code
function createSwatchButton(value, label, color) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'swatch-btn';
btn.setAttribute('aria-label', 'Color: ' + label);
btn.setAttribute('data-value', value);
var fill = document.createElement('span');
fill.className = 'swatch-fill';
fill.setAttribute('aria-hidden', 'true');
fill.style.backgroundColor = color;
btn.appendChild(fill);
return btn;
}
aria-pressed="true" on the selected button and update it dynamically when the selection changes. If you use radio inputs instead of buttons, aria-checked is handled automatically by the browser.
2. Category page add-to-cart and quick-view buttons that do not name the product
BigCommerce category and collection pages render product cards with action buttons below each card. In many theme implementations, these buttons say "Add to Cart" or show a cart icon, with no indication of which product they apply to. A screen reader user navigating a page with 24 products hears "Add to Cart" repeated 24 times, with no way to determine which product each button belongs to without navigating back up through the card markup.
Axe-core may flag this as a button-name violation if the button has no accessible text, or as a duplicate-accessible-name issue if all buttons share the same label. The WCAG criterion is 2.4.6 (Headings and Labels) and 4.1.2 (Name, Role, Value).
The standard fix is to include the product name in the button's accessible label. In Stencil templates, the product name is available via the {{name}} variable on a card context. In templates/components/products/card.html:
<!-- Before: button with no product context -->
<button class="button button--small card-figcaption-button"
data-event-type="product-click">
Add to Cart
</button>
<!-- After: button with product name in the accessible label -->
<button class="button button--small card-figcaption-button"
data-event-type="product-click"
aria-label="Add {{name}} to cart">
<span aria-hidden="true">Add to Cart</span>
</button>
The aria-label on the button overrides the button's text content as the accessible name. Screen readers announce "Add Merino Wool Scarf to cart" instead of "Add to Cart". The visible text stays the same for sighted users. Apply the same pattern to the quick-view button if your theme includes one:
<button class="button button--small card-figcaption-button"
data-product-id="{{id}}"
aria-label="Quick view {{name}}">
<span aria-hidden="true">Quick View</span>
</button>
3. Cart drawer and off-canvas panels without focus management
BigCommerce stores commonly use a slide-in cart preview drawer that opens when a product is added to the cart or when the cart icon in the header is clicked. WCAG success criterion 2.4.3 requires that when a modal or panel opens, keyboard focus moves into it. When the panel closes, focus must return to the element that triggered it. These two requirements -- focus on open, return focus on close -- are what screen reader and keyboard users depend on to understand that the context has changed.
In Cornerstone, the cart preview is handled in assets/js/theme/cart.js and related utilities. The panel is typically a div element that is shown and hidden via CSS class toggling. The fix requires explicit focus management in the JavaScript that controls the panel:
// Simplified example of focus management for an off-canvas cart panel.
// Adapt selectors to match your theme's actual markup.
var cartPanel = document.querySelector('[data-cart-preview]');
var cartTrigger = document.querySelector('[data-cart-preview-trigger]');
var lastFocused = null;
function openCartPanel() {
lastFocused = document.activeElement; // remember where focus came from
cartPanel.removeAttribute('hidden');
cartPanel.removeAttribute('aria-hidden');
// Move focus into the panel. Target the heading or close button.
var firstFocusable = cartPanel.querySelector(
'button, [href], input, [tabindex]:not([tabindex="-1"])'
);
if (firstFocusable) {
firstFocusable.focus();
}
}
function closeCartPanel() {
cartPanel.setAttribute('hidden', '');
cartPanel.setAttribute('aria-hidden', 'true');
// Return focus to where it came from.
if (lastFocused) {
lastFocused.focus();
}
}
The panel element itself should have role="dialog" and aria-label or aria-labelledby pointing to the panel's heading. When the panel is closed, add aria-hidden="true" so its contents are excluded from the accessibility tree:
<!-- In templates/components/cart/preview.html -->
<div
role="dialog"
aria-label="Cart"
aria-hidden="true"
data-cart-preview
class="previewCart">
<h2 id="cart-panel-title" class="previewCart-heading">Your Cart</h2>
<button type="button" class="previewCart-close" aria-label="Close cart">
<svg aria-hidden="true"><!-- close icon paths --></svg>
</button>
<!-- cart contents -->
</div>
The same focus management pattern applies to faceted search filter panels that open as off-canvas drawers on mobile. Each panel that covers or displaces page content needs the same open/close focus discipline.
4. Quantity inputs without associated labels
Product pages and cart pages in BigCommerce include quantity inputs that allow customers to set how many units to add or update. These are typically plain number inputs. In many Stencil themes, the visible "Qty" label is a separate element that is not programmatically linked to the input via for/id or aria-labelledby. A screen reader user navigating to the input hears only "1, spin button" or "1, edit text" -- the current value but not what it is a quantity of.
The Cornerstone product page template at templates/components/products/quantity.html or inline in templates/pages/product.html typically looks like:
<!-- Common pattern: label and input not linked -->
<div class="form-field">
<span class="form-label">Qty</span>
<div class="form-increment">
<button class="button--icon" data-action="dec">-</button>
<input type="tel" name="qty[]" value="1" min="1"
class="form-input form-input--small">
<button class="button--icon" data-action="inc">+</button>
</div>
</div>
The fix is a proper label element with a for attribute matching the input's id:
<!-- Fixed: label associated via for/id -->
<div class="form-field">
<label class="form-label" for="qty-input">Qty</label>
<div class="form-increment">
<button class="button--icon" data-action="dec"
aria-label="Decrease quantity">-</button>
<input type="tel" id="qty-input" name="qty[]" value="1" min="1"
class="form-input form-input--small">
<button class="button--icon" data-action="inc"
aria-label="Increase quantity">+</button>
</div>
</div>
Note the aria-label additions on the increment and decrement buttons. A button with only a minus or plus character has no accessible name by default. The label "Decrease quantity" and "Increase quantity" describe what each button does rather than relying on the icon character.
If you have multiple quantity inputs on the cart page (one per line item), each input needs a unique id. You can generate distinct IDs from the line item identifier available in the Handlebars context:
<!-- Cart page: unique id per line item -->
<label class="form-label" for="qty-{{id}}">
Quantity for {{name}}
</label>
<input type="tel" id="qty-{{id}}" name="qty[]" value="{{quantity}}" min="1"
class="form-input form-input--small">
5. Product image gallery navigation controls
BigCommerce product pages include a main product image with thumbnail navigation below or beside it. In Cornerstone and most derived themes, the thumbnails are li elements containing anchor or button elements. When these thumbnails have no accessible name, screen reader users cannot identify which image variant each thumbnail represents. When the main image changes in response to selecting a thumbnail, the change may not be announced.
Two fixes address the most common variants of this issue.
First, thumbnail buttons should be labeled with the image they represent. In templates/components/products/product-view.html or the carousel partial, add an accessible label to each thumbnail control:
<!-- Before: thumbnail with no label -->
<li class="productView-thumbnail">
<a href="#" class="productView-thumbnail-link" data-image-id="{{id}}">
<img src="{{getImage this 'thumbnail' '100x100'}}" alt="">
</a>
</li>
<!-- After: thumbnail labeled with the image description -->
<li class="productView-thumbnail">
<a href="#" class="productView-thumbnail-link"
data-image-id="{{id}}"
aria-label="View image: {{description}}">
<img src="{{getImage this 'thumbnail' '100x100'}}" alt="">
</a>
</li>
The alt="" on the thumbnail img is intentional -- the link already has an aria-label, so the image inside should be decorative from the accessibility tree's perspective. Having both would cause the label to be announced twice.
Second, when a thumbnail is clicked and the main image changes, a screen reader should be notified of the update. A live region handles this:
<!-- Add to the product view template -->
<div role="status" aria-live="polite" aria-atomic="true"
id="product-image-status" class="visually-hidden"></div>
// In your thumbnail click handler
function handleThumbnailClick(thumbnailElement) {
var description = thumbnailElement.getAttribute('aria-label')
.replace('View image: ', '');
// ... update the main image src ...
// Announce the change
var status = document.getElementById('product-image-status');
if (status) {
status.textContent = 'Now showing: ' + description;
}
}
The live region with aria-live="polite" announces its text content to screen readers when it changes, without interrupting the user mid-speech. aria-atomic="true" ensures the full content of the region is announced, not just the changed portion.
A note on the BigCommerce native checkout
BigCommerce's optimized one-page checkout is hosted on a separate checkout.bigcommerce.com subdomain and is maintained by BigCommerce directly. It is generally accessible at WCAG 2.1 AA, and store owners cannot modify its templates directly (only through the BigCommerce Checkout SDK for custom checkout implementations). If your scan findings are limited to the checkout page, they are likely platform-level issues that BigCommerce itself manages, not store-level template problems.
The issues described in this guide apply to storefront pages (homepage, category, product, cart) where the Stencil templates and theme JavaScript are under your control.
What monitoring adds
BigCommerce stores change frequently. New product images are added without alt text, promotional banners appear before seasonal campaigns, and theme updates from BigCommerce or your development team can introduce regressions in components that were previously correct. A static audit captures a point in time; monitoring catches new issues when they appear rather than when a complaint arrives.
If you want to see where your BigCommerce store stands right now, email hello@barrierscan.com with your store URL. We will run a full scan and send you the findings, at no cost, covering all the issue types described here plus the other patterns we measure across the 26 WCAG 2.1 criteria in our ruleset.