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?

Basic Shopping Cart
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.

Description Column
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:

Blank Column
  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.

Header It Does Not Span
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.

A <tfoot> Effort
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.

Restructured Shopping Cart
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.

No Spans
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:

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.

The Chrome issue where JAWS and NVDA announce the row header once for each column it spans. Which is why you hear: Sub dash total sub dash total sub dash total sub dash total, dollar one thousand five hundred and twenty five point zero zero, row 5.
Chrome accessibility inspector and tree focused on the cell that generates the extra announcement. There is no indication that its row header should announce four times.
I see no indication from Chrome’s accessibility tree that this would happen.
The Chrome team could not reproduce the issue in Chrome 100 with NVDA 2021.3, so I quickly made this uncaptioned video that shows the speech viewer, keys used, and browser / screen reader versions. Sloppy, but gets the job done. Added 25 January.

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.

JAWS announces all the row headers, and column header, of the wrong column when navigating horizontally into the cell that follows the spanning 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 .

I used green arrows to indicate the offending cells, but left the full Total column in for completeness. You can also hear VoiceOver announce the column headers as part of the SKU row which, well, whatever. I cheated and did not make closed captions, letting the VO speech viewer act as open captions. Also, the keys I embedded in the video are not accurate, but I cannot represent Mac option key in Camtasia for Windows.

4 Comments

Reply

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.

Ed gray; . Permalink
In response to Ed gray. Reply

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.

Reply

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.

Jason Burnett; . Permalink
In response to Jason Burnett. Reply

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).

Leave a Reply to Adrian Roselli Cancel response

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>