Under-Engineered Responsive Tables
I have written a bunch about responsive tables. Maybe too much. I keep trying to give developers the information they need to make informed decisions — ARIA attributes, screen reader & browser pairing results, bugs, and so on. I have spread things out over years of posts. I have filed issues and given talks and evangelized in meet-ups and…
Perhaps that is why I have had little traction.
This post is meant to simplify that. It will reference previous work, but mostly I am just going to show you the bare minimum you need to make a WCAG-compliant responsive HTML table. I have tested this with users, along with the version I made that reflows the content, and this one performs better for all users.
Mostly I am expanding a tweet thread.
HTML
You need two lines of HTML:
<div role="region" aria-labelledby="Caption01" tabindex="0">
<table>[…]</table>
</div>
The tabindex="0"
satisfies WCAG SC 2.1.1 Keyboard (A), which allows a keyboard-only user to tab to the container (giving it focus) and scroll it using the keyboard.
Anything that receives focus must have an accessible name and role that can be programmatically determined in order to satisfy WCAG SC 4.1.2 Name, Role, Value (A). The aria-labelledby
and role
attributes provide those, assuming Caption01
is the value of the id
attribute of the table’s <caption>
. I use region
as the role since it is a generic landmark.
Voice users may not know the accessible name to select the container, but they can at least still select it by asking to see all interactive elements on the page.
CSS
Then you need six lines of CSS:
[role="region"][aria-labelledby][tabindex] {
overflow: auto;
}
[role="region"][aria-labelledby][tabindex]:focus {
outline: .1em solid rgba(0,0,0,.1);
}
The overflow: auto
satisfies WCAG SC 1.4.10 Reflow (AA) by preventing the entire page from having two axes of scrolling (big table, small viewport).
The outline
covers WCAG SC 2.4.7 Focus Visible (AA), but to be safe on WCAG SC 1.4.11 Non-text Contrast (AA), use an outline
color with a 3:1 contrast ratio. Using outline
ensures it will be visible in Windows High Contrast Mode.
Note the CSS selector: [role="region"][aria-labelledby][tabindex]
This selector ensures the table will not be clipped unless the HTML is properly marked up to be accessible to keyboard and screen reader users. This approach is better than relying on a class
or id
as a selector since it helps enforce the needed HTML. You can tweak it to your needs, perhaps by using [tabindex="0"]
instead of [tabindex]
or [role]
instead of [role="region"]
, but it partly depends on what you need to enforce.
Usability Bonus
You can offer a visual cue for vertical scrolling with shadows from Lea Verou or horizontal scrolling shadows from Chen Hui Jing. This can be particularly handy for users on browsers that hide scrollbars by default (probably do not over-compensate with custom scrollbars). If you use this approach with non-RTL languages, you may need to add language / direction selector to swap between them.I put them to use in my post Fixed Table Headers (which is mostly a longer version of this post).
Example
A basic table wrapped in a scrolling container that only scrolls when it would otherwise overflow, embedded below or visit directly. Check the debug mode if you want it free of other Codepen code.
See the Pen MWeRJWd by Adrian Roselli (@aardrian) on CodePen.
Wrap-up
To set expectations, responsive in the web context generally means something adapts to viewport dimensions, typically width. This means a smart phone user and a zoomed desktop user will see the same view. This is not a proxy for touch, keyboard, nor other interaction types. These styles will likely need to go away for print as well.
Yes, you can apply CSS display
properties to reflow the table, but some browsers break the semantics and a screen reader user will be stranded (yes, even at the end of 2020 this is still true). You will then have to add the semantics back with ARIA roles.
ARIA cannot replicate all the semantics. For example, if you use spanning cells, ARIA has no attribute that maps to the HTML headers
attribute. If you have spanned content and are reflowing the table, things can get weird.
Whatever you do, avoid ARIA grid
roles for responsive tables (and similarly, avoid using <div>
s). ARIA grids are interactive widgets, not data tables (though they retain some traits of data tables). Just use HTML tables for your data.
Update: 4 December 2020
If you make a table that has no interactive bits in at all (no buttons for sorting, no links, no widgets, etc), and place it in the container I outlined above (giving it a tabindex="0"
), and an Android 9/10/11 user comes to the table using Chrome (or any Chromium browser, but not Firefox) with TalkBack, then the table will be skipped. It doesn’t matter if you use a keyboard, explore-by-touch, or just have the page read to you.
I filed a bug with the Android folks and (after some back-and-forth) it was confirmed and reported… up the chain? Anyway, go confirm or test or star or whatever the kids do with the platform bugs nowadays: #174769542: TalkBack does not recognize tables in parents with tabindex
Update to Other Update: 23 March 2021
This is an update on the previous update. Last night the bug got its own update, saying the TalkBack / Chrome bug would be fixed in the Chrome 91 update. That update should land after 25 May 2021, so be sure to update then. And now you are updated.
Update: 23 June 2021
This post does not use CSS scroll snap. That is intentional. Two reasons why I did not implement it:
- A Chrome bug from 2018, Issue 835301: [css-scroll-snap][position-sticky] scroll-snapping “stickily” positioned elements can cause inaccurate snap positions, which was not fixed with the Chrome 91 table fixes;
- In testing, users often wanted to straddle a cell to compare with others, and on smaller screens, higher zooms, or fuller cells, this became particularly difficult.
I forked the pen I use in my Fixed Table Headers post (where this impact is more pronounced), added scroll-snap-align: start
to the th
s and td
s, and added scroll-snap-type: both mandatory
to the wrapper. You will see in Chrome the caption gets clipped on vertical scroll and the second cell can be clipped on horizontal scroll.
Once you factor in users who resize text or zoom, things can get problematic. Test your scroll snap solution against WCAG SCs 1.4.4 Resize Text, 1.4.10 Reflow, and 1.4.12 Text Spacing. Then try it with just the keyboard.
See the Pen Fixed Table Header Demo: Responsive with Scroll Snap by Adrian Roselli (@aardrian) on CodePen.
If you want to use scroll snap, test the heck out of it both with users and in browsers.
Update: 9 June 2022
In my post Keyboard-Only Scrolling Areas, I explore other methods for a keyboard-only user to make scrolling areas scroll.
My recommendation at the end of that post is to continue to use the approach I outline above.
17 Comments
Friendly heads up, in the last paragraph of your CSS section, you have [tabindex=”0″] twice. I think you meant to say “perhaps by using [tabindex] instead of [tabindex=”0″]” to match the pattern you used for the role attribute. Great write up as always.
In response to .Fixed, thanks!
Great, I would have locked also the left part of the table
In response to .Nicola, in the section Usability Bonus above I reference another post of mine that shows you how to do just that:
I put them to use in my post Fixed Table Headers (which is mostly a longer version of this post).
It seemed redundant to include all that code in here again given the scope of this post.
Hi Adrian. You may also want to explicitly note that you add the parent
with
role="region"
so as to not overwrite the native semantics of the table element.
In response to .Nikolas, I assume you were referring to the parent
<div>
. I do not use the<div>
to avoid overriding the table semantics. The<div>
is there solely to act as the scrolling container for the table. At no point is the table at risk of having semantics overridden in this approach either by CSS display properties or by an errant ARIA role.The only reason the
<div>
gets a role is because as the scrolling container it can receive focus and:Anything that receives focus must have an accessible name and role that can be programmatically determined in order to satisfy WCAG SC 4.1.2 Name, Role, Value (A). […] I use
region
as the role since it is a generic landmark.
Lovely, comprehensive, and simple solution. Thanks for sharing!
This is great info! One issue; there isn’t a single accessible table in the entire article! The one inside the CodePen iFrame isn’t accessible via the keyboard.
In response to .John, there is only one table in the post, the one in the Codepen iframe. So do you mean the iframe cannot be accessed with the keyboard? Or the table’s wrapper? Or do you mean the table (which itself does not take keyboard focus)?
If it has anything to do with the latter two (not the Codepen iframe), then try the example in debug mode and let me know what browser you are using. Then I can see if I can re-create it.
Regarding the 2020-12-04 update, the Google issue tracker is awful beyond belief. There are two actionable items on the issue, star and flag. Neither of them means what I thought they mean, and with a touch interface that is only discoverable by doing the wrong thing first. I won’t hold my breath on that one. :-(
Very nice Adrian.
To cover the condition where the table width is less than its container, may I suggest:[role="region"][aria-labelledby][tabindex] { display: inline-block; max-width: 100%; overflow: hidden; overflow-x: auto; border: 0.1em solid #d6d6d6; }
Example: Font-size comparison table
In response to .Mike, I appreciate that you do not want the border to extend past the table. Using
display: inline-block
is a non-starter, however, since some browsers historically break the semantics of the table when encountering that style. This is still true (see slides 15 and 17). If you have a solution that does not use CSSdisplay
properties, then by all means give it a shot.
Having the region and the table with the same text could be a little confusing. Adding an aria-label and self-referencing using aria-labelledby would be a good way to give it more more complete description.
i.e.<div role="region" aria-labelledby="Caption01-region Caption01" tabindex="0" id="Caption01-region" aria-label="Scroling region for table: "> <table> <caption id="Caption01">Books in a Scrolling Container</caption>
It should read out, “Scrollable Region for table: Books in a Scrolling Container”
In response to .Glad to see the code black worked.
Here is my codepen for it.
https://codepen.io/Andy-NT/pen/bGgxRxK
In response to .Andrew, > and < have to be escaped (as
>
and<
), except for the allowed elements, otherwise my comment form eats them as a security precaution. I added what I think was the HTML you wanted by copying it from your CodePen example.I intentionally do not note the scrolling region in the accessible name. A blind screen reader user is unaffected and a sighted screen reader user has the visual cues. I also try to not be too verbose.
I compared our examples in JAWS and NVDA (my Mac is too far away). I tried reading from the top, using the virtual cursor, and using the Tab key. Regardless of approach, once I hear any announcement that tells me there is a table, I can start navigating in it. Or I can immediately press T if I am familiar with the pattern and start using the table.
Using my example with Chrome 89 / JAWS 2020:
- Reading from top:
table with 5 columns and 10 rows Books in a Scrolling Container…(the scrolling container is not announced)- With virtual cursor:
Books in a Scrolling Container(minus the table context; the scrolling container is not announced)- Pressing Tab:
Books in a Scrolling Container tableUsing your example with Chrome 89 / JAWS 2020:
- Reading from top:
table with 5 columns and 10 rows Books in a Scrolling Container…(the scrolling container is not announced)- With virtual cursor:
Books in a Scrolling Container(minus the table context; the scrolling container is not announced)- Pressing Tab:
Scroling region for table: Books in a Scrolling Container tableUsing my example with Firefox 87 / NVDA 2020.4:
- Reading from top:
region table with 10 rows and 5 columns caption Books in a Scrolling Container- With virtual cursor:
region table with 10 rows and 5 columns caption Books in a Scrolling Container- Pressing Tab:
region Books in a Scrolling Container table with 10 rows and 5 columns Books in a Scrolling Container captionUsing your example with Firefox 87 / NVDA 2020.4:
- Reading from top:
Scroling region for table: Books in a Scrolling Container region table Books in a Scrolling Container caption- With virtual cursor:
Scroling region for table: Books in a Scrolling Container region table with 10 rows and 5 columns caption Books in a Scrolling Container- Pressing Tab:
Scroling region for table: Books in a Scrolling Container region Books in a Scrolling Container table with 10 rows and 5 columns Books in a Scrolling Container captionNVDA seems to ignore announcing the region name if it is the same as the table. This may be good or bad, depending on user experience and how verbose you want to be.
None of this means either of our suggestions is right or wrong. Choosing the right text is a function of testing with your users to see which gives the most context without being verbose. Similar to how I provide a few announcement options in my post on sortable column headers.
Hello, I’m wondering what license this is released under (if any)? Thanks!
In response to .I added my standard license statement at the end of the post (I am not used to asserting licenses). Essentially the CC BY-SA 4.0 is there to prevent corporates from re-selling it in some weird way as their own. My effort to keep a level playing field.
Leave a Comment or Response