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.


You need two lines of HTML:

<div role="region" aria-labelledby="Caption01" tabindex="0">

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.


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


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.

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



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.

Jean Ducrot; . Permalink
In response to Jean Ducrot. Reply

Fixed, thanks!


Great, I would have locked also the left part of the table

Nicola Di Marco; . Permalink
In response to Nicola Di Marco. Reply

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; . Permalink
In response to Nikolas. Reply

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!

Sean; . Permalink

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

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. :-(

Eric; . Permalink

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

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 CSS display properties, then by all means give it a shot.

Leave a Comment or 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>