Skip to main content

Notes

Making Product Comparison Tables Accessible for E-Commerce

Quietramp ·

Making Product Comparison Tables Accessible for E-Commerce

Electronics, appliance, and any multi-variant product catalog tends to grow the same feature once it has more than a handful of similar SKUs: a “Compare” checkbox on each product card, a running counter showing how many items are queued up, and a side-by-side table that lays out specs for two or more products at once. It’s a genuinely useful shopping tool — and it’s built from three separate custom controls stacked together, each of which commonly ships with its own distinct accessibility failure.

Quick answer: an accessible comparison feature needs three things fixed independently. The “Add to Compare” toggle needs a real accessible name and an exposed checked state, not just a CSS class swap (WCAG SC 4.1.2 Name, Role, Value, Level A). The resulting table needs <th> header cells with scope so a screen reader announces a spec value together with both its product (column) and its attribute (row) — or vice versa — instead of reading bare numbers in sequence (WCAG SC 1.3.1 Info and Relationships, Level A). And the floating “Compare (3)” tray needs a live region, so adding or removing a product is heard, not just seen (WCAG SC 4.1.3 Status Messages, Level AA). None of the three fixes depend on each other, which also means fixing only one still leaves shoppers stuck on the other two.

This article is general accessibility information, not legal advice. Compliance with the ADA, WCAG, or any other accessibility law or standard is a legal determination; consult qualified counsel for legal risk assessment.

Failure one: a compare checkbox with no name or state

The control that starts the whole flow — usually a small checkbox or toggle icon sitting on or near each product card — is frequently built as a styled <div> or <span> with a click handler and a checked/unchecked CSS class, not a real form control. Visually it’s obvious what it does; programmatically, it has no role, no accessible name, and no exposed state, so a screen reader announces nothing useful when it receives focus — sometimes not even that it’s interactive.

WCAG SC 4.1.2 Name, Role, Value (Level A) exists for exactly this case. Its Examples section states the requirement plainly for custom controls: a page “uses custom widgets — such as toggle buttons, comboboxes, or disclosure widgets — implemented using a combination of HTML and ARIA, to make sure that they programmatically convey their accessible name, their role, and their current state and value.”

The WAI-ARIA APG Checkbox pattern supplies the concrete fix. Every checkbox needs “an accessible label provided by one of the following: visible text content contained within the element with role checkbox, a visible label referenced by the value of aria-labelledby… or aria-label” — and its state has to be kept in sync: “when checked, the checkbox element has state aria-checked set to true. When not checked, it has state aria-checked set to false.” The simplest fix is usually the cheapest one: use a real <input type="checkbox"> under the hood (native, free accessible name/state/keyboard support) and reserve the styling for its label, rather than reimplementing checkbox behavior from a <div>:

<label class="compare-toggle">
  <input type="checkbox" name="compare" value="sku-4471" />
  <span aria-hidden="true" class="compare-icon"></span>
  Add to Compare
</label>

If the design genuinely requires a custom element instead of a native checkbox, it still needs role="checkbox", aria-checked, a real accessible name, and Space-key toggle support — the APG pattern’s full requirement, not just the visual state change.

Failure two: a table that reads as an unlabeled grid

Once a shopper has two or more products queued, the comparison view is almost always a table: products across the top, spec attributes (price, weight, battery life, warranty) down the side, values filling the grid. If that table is built with plain <td> cells and no <th> elements — common, since visually a styled <td> grid looks identical to a properly-headed one — a screen reader reads the values in sequence with no way to say which product or which attribute any of them belongs to.

WCAG SC 1.3.1 Info and Relationships (Level A) is the relevant criterion, and it’s the same one that governs static spec tables and size charts: relationships conveyed visually — here, “this value belongs to this product and this attribute” — have to be programmatically determinable, not just visible from the grid layout. The W3C WAI Tables Tutorial gives the concrete technique: “header cells must be marked up with <th>, and data cells with <td>,” and “for tables with unclear header directions, define the direction of each header by setting the scope attribute to col or row”:

<table>
  <caption>Comparing 3 products</caption>
  <tr>
    <th scope="col">Spec</th>
    <th scope="col">Model A</th>
    <th scope="col">Model B</th>
    <th scope="col">Model C</th>
  </tr>
  <tr>
    <th scope="row">Price</th>
    <td>$129</td>
    <td>$159</td>
    <td>$149</td>
  </tr>
  <tr>
    <th scope="row">Battery life</th>
    <td>18 hrs</td>
    <td>24 hrs</td>
    <td>20 hrs</td>
  </tr>
</table>

A comparison table has one wrinkle a static size chart doesn’t: its columns change at runtime as products are added or removed from the tray. That’s not a separate accessibility requirement — it just means the <th scope="col"> markup has to regenerate correctly every time the table re-renders, not only on first load. If the table is rebuilt from a template each time a product is added, this is usually automatic; if it’s patched in place with JavaScript, it’s worth explicitly testing that a rebuilt column still carries its <th scope="col"> header rather than degrading to a plain <td> after the first update.

Failure three: a silent item counter

The last piece is the part that ties the other two together — a floating tray or bar, often fixed to the bottom of the screen, showing something like “Compare (3)” and a “View Comparison” button. Adding or removing a product updates that count visually. For a screen reader user who isn’t looking at that corner of the screen, the count updates in total silence unless it’s explicitly wired up to announce itself.

WCAG SC 4.1.3 Status Messages (Level AA) covers this directly. Its Intent: “to make users aware of important changes in content that are not given focus, and to do so in a way that doesn’t unnecessarily interrupt their work” — precisely the shape of a tray count that updates somewhere off-screen from wherever the shopper’s focus currently is. The Understanding document’s own worked example is close to identical to this pattern: “after a user presses an Add to Shopping Cart button, a section of content near the Shopping Cart icon adds the text ‘5 items’. A screen reader announces ‘Five items’ or ‘Shopping cart, five items.’” The same mechanism — a role="status" region (implicit aria-live="polite") wrapping the count text — applies directly to a compare tray:

<div role="status" class="compare-tray-count">
  3 items added to compare
</div>

Keep the announcement in the count element itself, not on the checkbox that triggered it — a screen reader user who checks three boxes in a row should hear the running total update each time, not three identical “added” announcements that don’t say how many are queued.

Don’t strand focus when the tray empties

One practical detail worth building in from the start, even though it isn’t a single named success criterion the way the three failures above are: each item in the compare tray typically has its own small “remove” (×) button. If a shopper removes an item while focus is on that item’s remove button, and the button itself is what gets deleted from the DOM, focus has nowhere defined to land — behavior varies by browser and screen reader, and the safest outcomes (falling back to <body>) are still a jarring, disorienting jump. Move focus deliberately instead: to the next remaining item’s remove button, back to the product card’s “Add to Compare” checkbox that originally added it, or to the tray’s own container if the removal just emptied it.

Where to start

In rough order of shopper impact: fix the “Add to Compare” checkbox first (swap in a native <input type="checkbox"> where possible — usually a small, contained change), then the table headers (a template change that, once fixed, applies to every comparison view automatically), then the tray’s live-region count, then the remove-button focus handling.

FAQ

Does the compare checkbox need to be a native <input type="checkbox">, or is a custom widget fine? Native is the cheaper fix — it gets accessible name, state, and keyboard support for free. A custom widget is fine too, but only if it fully implements the WAI-ARIA APG Checkbox pattern’s role, accessible name, aria-checked state, and Space-key handling; a visual-only toggle doesn’t meet SC 4.1.2 regardless of how it looks.

Is scope="col"/scope="row" enough for a comparison table, or do I need headers/id? scope is sufficient for the standard shape — one header row for products, one header column for attributes. headers/id association is only needed if a comparison table ever adds spanning or multi-level headers, which isn’t the typical layout for this feature.

Do I need to announce every single “added to compare” click, or just the count? The count is what matters under SC 4.1.3 — a shopper needs to know how many items are queued, not a running transcript of every click. Wrap the count text itself in a role="status" region rather than trying to announce each add/remove action separately.

Get a full audit of your comparison feature

This covers one specific, common e-commerce pattern — a full Quietramp audit checks your whole site against WCAG 2.1 AA, verified by a person operating it with a keyboard and screen reader, not just an automated scan (which will typically flag the missing checkbox label and stop there, without catching the silent tray count or the unlabeled table cells on the same page). See a real sample report, or check pricing — $890 one-time, $99/month for ongoing re-checks after fixes ship.


This is educational content about technical accessibility standards, not legal advice. Meeting WCAG 2.1 AA does not guarantee legal compliance with the ADA or any other law, and nothing here should be read as a compliance guarantee. Consult qualified counsel for legal risk questions.

This article was drafted with AI assistance and reviewed by a person for accuracy before publication.

← Back to Notes