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.
Others in this sorta-series:
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.
You need two lines of HTML:
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 <a href="https://www.w3.org/TR/wai-aria-1.1/#aria-labelledby">aria-labelledby</a>
and role
attributes provide those, assuming Caption01
is the value of the id
attribute of the table’s <caption>
. I use <a href="https://www.w3.org/TR/wai-aria-1.1/#region">region</a>
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.
Then you need six lines of CSS:
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.
I put them to use in my post Fixed Table Headers (which is mostly a longer version of this post).
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.
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.
accessibility, html, pattern, standards, tables, WCAG
Earlier post: a11yTO Conf: CSS Display Properties versus HTML Semantics
More recent post: Don’t Rely on YouTube Transcripts
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.
Fixed, thanks!
Great, I would have locked also the left part of the table
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.
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 <a href="https://www.w3.org/TR/wai-aria-1.1/#region" rel="nofollow ugc">region</a>
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.
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.