Accessible Cart Tables?
The online holiday shopping coupled with my need to make a new invoice template got me looking at a common table structure that is harder to expose to screen readers than it seems at first glance. One I first coded in, checks watch, 1997 when I was an ecommerce developer (if you bought Microsoft, Adobe, Adaptec, Starfish, or other products online back then, you’re welcome).
The structure for this cart is straightforward. A column each for the SKU, description, price, quantity, and row total. Each row is a line-item in the cart, and the product description is the row header.
It gets a bit more complex when you want to have a row for sub-total, tax, shipping, and grand total. They typically span all but the last column and they each need row headers. But from where?
SKU | Description | Unit Price | Quantity | Total |
---|---|---|---|---|
123456 | Pudding | $1.00 | 240 | $ 240.00 |
987654 | A new set of steak knives | $90.00 | ½ | $ 45.00 |
456789 | Sand | $5.00 | 200 | $ 1,000.00 |
Sub-Total | $ 1,525.00 | |||
Shipping | FREE | |||
Grand Total | $ 1,525.00 |
The problem with this construct is that Sub-Total, Shipping, and Grand Total are all under the SKU column header by default. They are not SKUs. So that is a lie.
As an added bonus, JAWS/Chrome announces the last column in those three rows a bit oddly. If you navigate down the Total column into the Sub-Total row, you will hear sub dash total sub dash total sub dash total sub dash total
. JAWS is announcing the row header once for each column it spans.
As an extra added bonus, VoiceOver/Safari/macOS announces the first product row’s row header as shipping
and the second product row’s row header as grand total
. The third product row gets no love, announcing simply as row 4 of 7
(it is the fourth row of the table, third row of products). It never announces the real row header.
A general annoyance with navigating tables with spanning rows in VoiceOver/Safari/macOS is when you move down a column to a spanning cell that starts from a different column. Instead of the virtual cursor moving into the spanned cell it moves to the top of the next column. A skilled day-to-day user may appreciate this. I do not know if this is a feature, quirk, or bug.
Description Column with a headers
Attribute
The headers
attribute allows us to associate a spanning cell with a specific column header. SKU definitely does not apply, and Unit Price and Quantity are not a fit. But we can make a case for Description, so let’s try that.
SKU | Description | Unit Price | Quantity | Total |
---|---|---|---|---|
123456 | Pudding | $1.00 | 240 | $ 240.00 |
987654 | A new set of steak knives | $90.00 | ½ | $ 45.00 |
456789 | Sand | $5.00 | 200 | $ 1,000.00 |
Sub-Total | $ 1,525.00 | |||
Shipping | FREE | |||
Grand Total | $ 1,525.00 |
TalkBack/Chrome, Narrator/Edge, and JAWS/Chrome do not care and still announce the Sub-Total, Shipping, and Grand Total cells as under the SKU column. This is a known bug in Chromium (Issue 1081201: headers attribute ignored in tables resulting in an incorrect screen reader experience). VoiceOver/iOS no longer announces the column header, so it kind of works. NVDA/Firefox and VoiceOver/Safari/macOS do as we hope, announcing each as under Description.
This does nothing for the repeating row header JAWS bug, but there is no reason it should.
The VoiceOver/Safari/macOS bug persists, unsurprisingly.
Blank Column with a headers
Attribute
One approach is to insert a visually-hidden blank column and explicitly associate that with our spanning cells using the headers
attribute:
SKU | Description | Unit Price | Quantity | Total | |
---|---|---|---|---|---|
123456 | Pudding | $1.00 | 240 | $ 240.00 | |
987654 | A new set of steak knives | $90.00 | ½ | $ 45.00 | |
456789 | Sand | $5.00 | 200 | $ 1,000.00 | |
Sub-Total | $ 1,525.00 | ||||
Shipping | FREE | ||||
Grand Total | $ 1,525.00 |
Our spanning cells still announce the first column, but since it has a blank column header that announcement is silence (except for Narrator/Edge, which announces the column header as space
). Now we have a table with one extra column announced than is visible, with empty cells, and with a total column count that is essentially off by one. That is a lie.
The Chromium bug only feels like it is bypassed because the blank value is in the first column, not because it suddenly honors headers
.
The JAWS repeating row header bug persists on those last three rows, announcing one more instance because we have added one more column.
The VoiceOver/Safari/macOS bug is still there.
Using a headers
Attribute to Point to a Header It Does Not Span
These three spanning rows need to be associated with a column header. So why not the one that most closely describes what they are, the Total? It is part of the table and will not throw off the count. The problem is that the rows do not span into the Total column (since that is where the totals live). So let’s force it with the headers
attribute.
SKU | Description | Unit Price | Quantity | Total |
---|---|---|---|---|
123456 | Pudding | $1.00 | 240 | $ 240.00 |
987654 | A new set of steak knives | $90.00 | ½ | $ 45.00 |
456789 | Sand | $5.00 | 200 | $ 1,000.00 |
Sub-Total | $ 1,525.00 | |||
Shipping | FREE | |||
Grand Total | $ 1,525.00 |
That doesn’t work either. While NVDA/Firefox participate in our ruse, JAWS/Chrome and VoiceOver/iOS still announce them as under the SKU column. VoiceOver/Safari/macOS, TalkBack/Chrome, and TalkBack/Firefox do not announce the column header name, but they all still announce them as living in column 1.
The Chromium bug is clearly still there.
JAWS helpfully persists with its quadruple row header announcement bug.
The VoiceOver/Safari/macOS bug cares little for this effort, and carries along being wrong.
Using <tfoot>
and aria-labelledby
for Last 3 Rows
Now we’re getting desperate. And what do desperate people do? Throw ARIA at it!
We don’t want to override any specific column headers with ARIA, as that will mess up the entire table experience. So we’ll wrap the spanning rows in a <tfoot>
, give it an aria-labelledby
that points to the Total column header (which will just bring the text node value, no structural info), and hope for the best.
SKU | Description | Unit Price | Quantity | Total |
---|---|---|---|---|
123456 | Pudding | $1.00 | 240 | $ 240.00 |
987654 | A new set of steak knives | $90.00 | ½ | $ 45.00 |
456789 | Sand | $5.00 | 200 | $ 1,000.00 |
Sub-Total | $ 1,525.00 | |||
Shipping | FREE | |||
Grand Total | $ 1,525.00 |
NVDA/Firefox, JAWS/Chrome, VoiceOver/iOS, VoiceOver/Safari/macOS are unmoved, announcing the SKU column header.
TalkBack/Chrome does not announce the row headers, just the position; however, it announces the Sub-Total, Shipping, and Grand Total cells as Total
and then announces they are in the SKU column. TalkBack/Firefox announces Total, row header
and then the cell contents, but since TalkBack/Firefox never announces row or column headers, this is not necessarily a win.
Narrator/Edge also announces the Sub-Total, Shipping, and Grand Total cells as Total
and then announces they are in the SKU column.
VoiceOver/Safari/macOS announces the Sub-Total, Shipping, and Grand Total cells as their text while also under the SKU column. However, as we move down the dollar values in the Total column it announces the row header for each as Total, ignoring the visible text values of Sub-Total, Shipping, and Grand Total.
The Chromium bug does not come into play here because there is no headers
attribute.
That JAWS bug? Now it announces total total total total
as you move down the Total for those last three rows.
The VoiceOver/Safari/macOS bug is unmoved by the <tfoot>
, probably because it does not affect the first two product rows that it clearly hates.
Restructure the Table
Maybe this isn’t something we can solve with spurious value and association assignments. Maybe we just need to restructure our table. In this case, we add a Sub-Total column and use the headers
attribute to associate our spanning cells with that new column header.
SKU | Description | Unit Price | Quantity | Sub-Total | Total |
---|---|---|---|---|---|
123456 | Pudding | $1.00 | 240 | $ 240.00 | $ 240.00 |
987654 | A new set of steak knives | $90.00 | ½ | $ 45.00 | $ 45.00 |
456789 | Sand | $5.00 | 200 | $ 1,000.00 | $ 1,000.00 |
Sub-Total | $ 1,525.00 | ||||
Shipping | FREE | ||||
Grand Total | $ 1,525.00 |
Our table is wider. And adds more visual complexity. And adds more data than we need.
But for those users who dwell on the row header cells of those last three rows, at least their column header makes more sense now.
But is this really a win?
The Chromium and VoiceOver/Safari/macOS bugs keep on keeping on.
Dump All Spans
Spanning columns is just an artifact of forcing orphaned data points into an artificial construct meant to ease visual layout and auditory experiences while absolving developers of the responsibility of creating a more meaningful structure that would then be ported to HTML for standardization is what we are telling ourselves as we throw up our hands and dump all the spanning cells.
SKU | Description | Unit Price | Quantity | Total |
---|---|---|---|---|
123456 | Pudding | $1.00 | 240 | $ 240.00 |
987654 | A new set of steak knives | $90.00 | ½ | $ 45.00 |
456789 | Sand | $5.00 | 200 | $ 1,000.00 |
Sub-Total | $1,525.00 | 1 | $ 1,525.00 | |
Shipping | FREE | 1 | $ 1,525.00 | |
Grand Total | $ 1,525.00 |
Now it’s just a normal table. All the screen readers behave because there is nothing special about it. Visually, it is a bit weird to have unit prices and quantities for these things, though with some careful styling you can make it seem less disjointed.
It certainly involves more key presses for a screen reader user if they get into those rows, but at least there is no confusion for which cell lives under which column (even the quiet blank ones). This can be a bit annoying for screen readers that do not announce the row headers in any of these constructs, such as Narrator/Edge, TalkBack/Firefox, TalkBack/Chrome, and VoiceOver/iOS.
The lack of row header announcement also makes it difficult to understand the Sub-Total and Grand Total dollar values are not from additional products, though the Shipping column sort of is. Those extra key presses add a bit of friction to comprehending that disconnect.
But hey, that JAWS and Chromium bugs are moot and VoiceOver/macOS is no longer announcing the wrong row headers (by not announcing any at all).
Go Back to the Start and Test with Humans Instead
That second table is looking a bit less awful now, yeah?
For unsupporting screen reader / browser pairings, since SKU is nice and short, and few users may think “Sub-total SKU” means there is a new product in the cart (though for shipping it is somewhat accurate), you may want to get this construct in front of users for feedback.
You may even be surprised to find most don’t care. Screen reader users in particular have experienced some truly awful interfaces. The more skilled among them will not be slowed down. But you need to confirm if or how this can be a burden for your audience.
The point is that HTML is not always ideal for even straightforward scenarios. Throwing ARIA or lots of extra structure doesn’t always make things better.
All this aside, if you have come up with a scenario that works, then please share. Along with context. And how you tested it.
My testing suite:
- Firefox 94–96 / NVDA 2021.2
- Chrome 96–100 / JAWS 2021–2022
- Edge 96–98 / Narrator / Windows 10
- Safari 15.1–15.2 / VoiceOver / macOS 12.01–12.1
- Safari/iOS/iPadOS 15.1 / VoiceOver / iPadOS 15.1
- Chrome 90–96 / TalkBack 9.1 / Android 12
- Firefox 92–98 / TalkBack 9.1 / Android 12
Embedded Examples
These tables exist as a CodePen that you can try outside of the context of my site, also in a cruft-free debug mode.
See the Pen Tables for Shopping Carts by Adrian Roselli (@aardrian) on CodePen.
Chromium Bug (added 24 January 2022)
The following video shows how JAWS struggles with the spanning row header. This same thing happens with NVDA and Chrome as well, so it is not unique to JAWS. I filed a Chromium issue at Issue 1290375: Screen readers announce spanning row header as many times as columns it spans.
JAWS Bug (added 24 January 2022)
In Firefox, JAWS struggled with understanding which columns held the cells that followed the spanning cells. It announced them as the wrong column and read all the row headers of that mis-identified column as the column header for the cell. I filed a JAWS issue at #601 JAWS announces wrong column header info after spanned cell.
Safari/WebKit Bug (added 27 January 2022)
VoiceOver announces the first and second product row headers as Shipping
and Grand Total
respectively. This can be confusing when a user hears what they expect to be a product and instead hear one dollar shipping, and then a ninety dollar grand total, which are then announced with new values in the same table but no corresponding product. I filed a WebKit issue, 235740 – AX: Incorrect row header announcement in VoiceOver macOS .
6 Comments
Why do the shipping and totals need to be included in the table data? The table is designed to contain the product info quantities and prices.
The subtotal, shipping and grand total are separate pieces of information that can exist outside of the table perfectly well. I understand that it looks neat from a visual perspective but I’m pretty sure you could achieve the same neatness by styling the table & totals as a grid/sub-grid. That doesn’t affect screen readers AFAIK.
In response to .Why do the shipping and totals need to be included in the table data?
They don’t. Yet I have had clients, vendors, platforms, libraries, etc. insist on doing it for decades. This post is an exploration of how to make that pattern less bad.
I’m pretty sure you could achieve the same neatness by styling the table & totals as a grid/sub-grid. That doesn’t affect screen readers AFAIK.
You could use CSS grid and sub-grid, when support for sub-grid is extant. As for how or if it affects screen readers, unless you plan to re-create all the necessary table roles so a screen reader user can still navigate the table as a native table, you would be creating a potentially problematic or annoying experience. If you cannot guarantee your audience is all on more recent browsers, then setting
display
properties can trigger some well-documented browser bugs. Given how many disabled users rely on older kit (for a variety of reasons), I would not make that gamble yet.
Isn’t there a mechanism for collapsing columns with no data? perhaps the columns in the footer without applicable values could be collapsed, resulting in the desired effect. How do collapsing columns work with readers? Surely blanks in the unit price, and the quantity would be collapsed into their preceding column, description. In which case the total, listed as subtotal works, and the description auto-expands to 3 columns. It would be styled differently (right-aligned) but could provide the preferred presentation.
Also, the total count value should be the total count, shouldn’t it? so if there are 530 of one item, and 333 of another, the subtotal would be 863, not one. This would help quickly approximate the average charge for each item, whereas 1 is misleading.
Thanks for this great exploration.
In response to .Isn’t there a mechanism for collapsing columns with no data?
The options are either leaving it blank or spanning it.
How do collapsing columns work with readers?
If you try to remove it completely (
aria-hidden
) it will throw off column counts and column header announcements throughout the table. Otherwise a spanned column or empty column announces as in my video samples.Also, the total count value should be the total count, shouldn’t it?
This entire demo uses fake data with no effort to be accurate to real products. There is no Total Count column or row, just total costs. If your use case demands a total count, then grab the demo that works best for your audience and follow the pattern to add that column (or row).
Hi Adrian,
I am just wondering if there has been any update with either (or both) the screen readers or the browsers that you may know of since your previous posts? I am wanting to update my checkout table so that it is accessibility-friendly, and wondering if there is by any chance a method that now suits all? Or if not, which is most recommended in your opinion?
In response to .There are always updates. For example, the latest JAWS (JAWS 2025, because Freedom Scientific does not allow linking to specific updates and relies on a 3.3.2-failing select menu) has some improvements to navigating and announcing features of tables. Those don’t apply too much here, however.
I wrote this post to provide testable patterns, instructions for how to test, bug references, and examples of output so that readers could stay current on their own. Your question is too general for me to know what kinds of changes would suit “all” users (given accessibility is a continuum that means different things to different users).
If you are not in a position to test, then go to the bugs I linked, see if they are still open, and if they are, ask when the heck they’ll get fixed.
Leave a Comment or Response