Functions to Add ARIA to Tables and Lists
When I presented my talk CSS Display Properties versus HTML Semantics at WordCamp Buffalo, someone in the crowd asked if I could take all I demonstrated and make it into a plug-in. I said no.
Generally when I write and present things, I provide techniques, ideas, examples, and experience to address issues. When I write copy-paste-ready code, with a few older exceptions, I write generic code not tied to a framework nor library.
I am not writing a WordPress plug-in. I have, however, written a couple vanilla JavaScript functions that you can drop into your site. They might benefit from some optimizations, and how you call the functions is up to you (and your chosen platform).
I also chose to address only the two most common cases that I see of browsers breaking semantics due to CSS — tables and lists. As I note elsewhere, the ARIA you add will not save you from display: contents
.
If / when the browsers ever fix their bugs, then you should purge these functions from your projects. So, you know, put that it in your backlog I guess.
Tables
The function walks through each <table>
on a page and adds the appropriate table role and roles for all its standard descendant elements. It also identifies any row headers by the scope
attribute (so make sure you use them correctly).
The final two example tables in the embedded Codepen come from the W3C examples for accessible complex tables.
Embedded Example
If the embed does not work, you can visit it directly.
See the Pen Table Automated ARIA Role Assignment by Adrian Roselli (@aardrian) on CodePen.
JavaScript Function
The function, ready for you to excoriate in the comments or copy into your own project:
function AddTableARIA() {
try {
var allTables = document.querySelectorAll('table');
for (var i = 0; i < allTables.length; i++) {
allTables[i].setAttribute('role','table');
}
var allCaptions = document.querySelectorAll('caption');
for (var i = 0; i < allCaptions.length; i++) {
allCaptions[i].setAttribute('role','caption');
}
var allRowGroups = document.querySelectorAll('thead, tbody, tfoot');
for (var i = 0; i < allRowGroups.length; i++) {
allRowGroups[i].setAttribute('role','rowgroup');
}
var allRows = document.querySelectorAll('tr');
for (var i = 0; i < allRows.length; i++) {
allRows[i].setAttribute('role','row');
}
var allCells = document.querySelectorAll('td');
for (var i = 0; i < allCells.length; i++) {
allCells[i].setAttribute('role','cell');
}
var allHeaders = document.querySelectorAll('th');
for (var i = 0; i < allHeaders.length; i++) {
allHeaders[i].setAttribute('role','columnheader');
}
// this accounts for scoped row headers
var allRowHeaders = document.querySelectorAll('th[scope=row]');
for (var i = 0; i < allRowHeaders.length; i++) {
allRowHeaders[i].setAttribute('role','rowheader');
}
} catch (e) {
console.log("AddTableARIA(): " + e);
}
}
AddTableARIA();
Update
ARIA 1.2, still in draft as of this writing, has a caption
role that maps to the native <caption>
element. The function has been updated to include it.
Lists
The function walks through each <ol>
, <ul>
, and <dl>
on a page and adds the appropriate list role and roles for all the required descendant elements.
Because ARIA does not have roles that differentiate between ordered and unordered lists, let alone description lists, avoid using this function unless you know the semantics of your list are broken. Even then only apply it to the list type affected. Note that for <dt>
and <dd>
I have two roles, in a bit of progressive enhancement.
Embedded Example
If the embed does not work, you can visit it directly.
See the Pen List Automated ARIA Role Assignment by Adrian Roselli (@aardrian) on CodePen.
JavaScript Function
The function, ready for you to eviscerate in the comments or copy into your own project:
function AddListARIA() {
try {
var allLists = document.querySelectorAll("ol, ul");
for (var i = 0; i < allLists.length; i++) {
allLists[i].setAttribute("role", "list");
}
var allListItems = document.querySelectorAll("li");
for (var i = 0; i < allListItems.length; i++) {
allListItems[i].setAttribute("role", "listitem");
}
var allDefLists = document.querySelectorAll("dl");
for (var i = 0; i < allDefLists.length; i++) {
allDefLists[i].setAttribute("role", "associationlist list");
}
var allDefTerms = document.querySelectorAll("dt");
for (var i = 0; i < allDefTerms.length; i++) {
allDefTerms[i].setAttribute("role", "associationlistitemkey listitem");
}
var allDefItems = document.querySelectorAll("dd");
for (var i = 0; i < allDefItems.length; i++) {
allDefItems[i].setAttribute("role", "associationlistitemvalue listitem");
}
} catch (e) {
console.log("AddListARIA(): " + e);
}
}
AddListARIA();
Update
ARIA 1.2, still in draft as of this writing, has an associationlist
role that maps to the native <dl>
element. In addition, it has an associationlistitemkey
role mapping to <dt>
and associationlistitemvalue
role mapping to <dd>
. The function has been updated to include these.
Wrap-up
If you end up converting this to the framework du jour, please drop a link to your repo hub in the comments.
Update: May 18, 2018
I have had the opportunity to test this function against a new technique for making tables responsive. Lea Verou has demonstrated a way to use text shadows to avoid duplicating content in her post Responsive tables, revisited. My opinion on this approach notwithstanding, it renders the tables unusable in a screen reader.
I took the table ARIA function from above, pasted it into the JavaScript block of the Dabblet editor for the example from her post, and ran through it again using NVDA. This time I was able to interact with the table using the NVDA table navigation commands. If you just listen to the video below, you will hear that it follows the same navigation path as the first video above.
Update: July 19, 2018
I updated the function for lists to include the roles term
for <dt>
and definition
for <dd>
. Support is still meh, which is why I originally used listitem
to be safe.
ARIA allows a space-separated list of roles, with the first supported one applied. I avoided this approach because of some bugs in screen readers with multiple roles, but three years on my testing suggests that for the majority of recent releases it is safe.
If you encounter troubles with this progressive enhancement, just yank one or the other role from each until things work.
Update: January 13, 2019
A Twitter thread kicked off a discussion about how Safari hands list information to VoiceOver based on how list item styles are used. Scott O'Hara did a great job capturing the conversation and the concerns about throwing ARIA at it in his post "Fixing" Lists.
You can use my functions above to try to brute-force VoiceOver to announce lists, but for VoiceOver users who already have experience with your site maybe don't do that. Look to the CSS solutions that Scott cites first, then maybe consider ARIA. James Craig has already filed an issue with Safari to update its heuristics.
I think Roger summed up the current situation nicely:
That so many didn’t know about the effects of CSS on list and table semantics in screen readers actually surprised me.
Update: January 22, 2020
The ARIA 1.2 draft includes roles for <caption>
as caption
, <dl>
as associationlist
, <dt>
as associationlistitemkey
, and <dd>
as associationlistitemvalue
. The functions, examples, and descriptions above have been updated even if the spec and browser support are not complete.
Update: February 19, 2020
Big progress. Chrome 80 no longer drops semantics for HTML tables when the display
properties flex
, grid
, inline-block
, or contents
are used. The new Edge (ChromiEdge) follows suit. Firefox still dumps table semantics for only display: contents
. Safari dumps table semantics for everything.
Chrome v80 no longer dumps table semantics when CSS flex is applied.
Test: cdpn.io/aardrian/debug/xxGEKKJ
Not only display property impact parity with Firefox, but passes Firefox (since display: contents still eats table semantics in Firefox but no longer in Chrome). pic.twitter.com/4iuitwgWhS
Update: September 29, 2020
Léonie Watson has just posted How screen readers navigate data tables where she walks through a sample table to get some information, explaining each step, the keyboard commands, and the output. She also links to a video demonstration, which I have embedded below.
Update: August 24, 2023
If you came here from the Kevin Powell responsive table tutorial on YouTube, I have some additional links about accessible tables. If you have no idea what I am talking about, here is the video (links follow it):
Other posts on my site that go into accessibility bits related to tables:
- Uniquely Labeling Fields in a Table
- Table with Expando Rows
- Fixed Table Headers
- Block Links, Cards, Clickable Regions, Rows, Etc.
- Sortable Table Column Mad Libs
- Under-Engineered Responsive Tables
- Sortable Table Columns
- Multi-Column Sortable Table Experiment
- Scroll Snap Challenges
- Accessible Cart Tables?
- Column Headers and Browser Support
- It’s Mid-2022 and Browsers (Mostly Safari) Still Break Accessibility via Display Properties
- Brief Note on Calendar Tables
- Avoid Spanning Table Headers
Update: 7 October 2023
Copied from It’s Mid-2022 and Browsers (Mostly Safari) Still Break Accessibility via Display Properties.
Very good progress in Safari 17. Tables and description lists are no longer broken when display
properties are applied. Buttons with display: contents
, however, are still inoperable by keyboard users and problematic for VO users (and I confirmed is also the case in Safari TP 180).
Meanwhile, the heading issue I reported for Safari 17 on iPadOS (261978 - AX: Headings with `display: contents` cannot be navigated) has been marked as a VoiceOver issue with no insight when it will be fixed. But marking it a VoiceOver issue means Safari can claim to have no bug so yay?
Apple seems reasonably confident it has finally fixed its historically years-lagging support (despite prior claims), and so has been doing the rounds suddenly arguing all the other browsers and specs need to fix display: contents
issues while using its own claims of (abruptly and questionably) better support to bolster them:
- CSS Working Group Comment on #3040 [css-a11y][css-display] display: contents; strips semantic role from elements from 2018.
- Web Platform Tests #568
display: contents
I also filed a PR with Can I Use to amend the one filed by Apple three weeks ago (I was unable to review owing to travel and this is not my job slash no one is paying me).
With Safari almost there on basic support and Apple now pushing for the specs and browsers to agree, after sitting it out for a few years, I am excited that the end is in sight. Which I expect before WCAG 3.
10 Comments
Hi Adrian, I appreciate you providing these helper functions. I want to ask the reason for adding redundancy to existing tabular and list markup. These elements already have implicit table and list roles that do not need to be explicitly set using ARIA. Thoughts? What would be really helpful is determining a way to fix mock tables and lists that use DIVs and other formatting elements. That would be fun! Keep up the good work, Adrian!
In response to .Nick, in my post Tables, CSS Display Properties, and ARIA I demonstrate how changing the display properties of tables causes browsers to blows away all the native semantics. Adding ARIA puts semantics back on the tables. You can see this in action in the videos in the first update above. This also applies to lists, but I have not written a post dedicated to lists.
As for using other elements in place of lists and tables, I encourage all developers not to do that. See more in my post Hey, It’s Still OK to Use Tables.
In response to .Hey, thanks for the reply. I’ll research the usage and implication of the display property and tables. I had no idea that the semantic information was stripped… I’ll have to check out what information MSAA Object Inspect is presented and test with NVDA. Oh, and we definitely advocate using tables for tabular data, but we still get devs go alternative routes so we have ways to fix that. Thanks, again!
In response to .I suspect you will find the information is stripped by Edge and doesn’t make it to MSAA, so it never makes it NVDA. At the very least I spoke with the Edge team about this and the more dangerous impact of
display: contents
, which Edge does not yet support, but which I hope when they do they do it right.
Performance tweak: once you have
allTables
, you don’t have to search for the needlesallRowGroups
in the almighty haystack called DOM using
document.querySelectorAll('thead, tbody, tfoot')
,
but inside the loop in
allTables[i].querySelectorAll('thead, tbody, tfoot')
.This applies to
allRows
inallRowGroups
, andallCells
&allHeaders
inallRows
respectively.Once you got
allHeaders
you might check if they matchth[scope=row]
, no need to search again forth[scope=row]
afterwards.I’ve forked it in this pen.
I haven’t measured the difference, though.
In response to .Gunnar, great points. If I have already walked the DOM, then why walk it again when I can just walk the sub-tree I already parsed.
Superb stuff. Thanks a ton!
For what it’s worth. I’ve spend quite a bit of effort trying to write a better program for the table ARIA function. I believe it looks a bit more clean and it also seems to runs a bit faster.
document.querySelectorAll("table").forEach(function (table) {
table.setAttribute("role", "table");
table.querySelector(":scope > caption")?.setAttribute("role", "caption");
table.querySelectorAll(":scope > thead, :scope > tfoot, :scope > tbody")
.forEach(tpart => tpart.setAttribute("role", "rowgroup"));
Array.from(table.rows).forEach(function (row) {
row.setAttribute("role", "row");
Array.from(row.children)
.forEach(cell => cell.setAttribute("role",
(cell.nodeName === "TD") ? "cell"
: (cell.scope === "row") ? "rowheader"
: "columnheader")
);
});
});
Hello Adrian – I came across your blog from watching Kevin Powell videos. I’m interested in improving accessibility via google chrome extension and one use case came to mind which I would highly value your input. Essentially it’s a municipal website for NYC public land records that is responsible for displaying land records in table format but it was built so long ago that it uses table elements for formatting. I understand the benefit and approach of adding accessibility markup to table elements that lack it but what is a solution (or potential solutions) to adding markup to a table element that essentially contains the name of the department sponsoring the website? I copied one of the table elements from the website and included it below. I’m interested in your thoughts. Thanks for your content!
<table border="0" cellspacing="0" cellpadding="0" width="100%">
<tbody><tr bgcolor="#999999">
<td height="2" colspan="2">
<div align="center">
<font face="Verdana, Arial, Helvetica, sans-serif"><b><font color="#FFFFFF" size="2">
New York City Department of Finance</font></b></font></div>
</td>
</tr>
</tbody>
</table>
In response to .Arthur, if you have an HTML table used strictly for layout, you can add
role="presentation"
to the<table>
element. This will essentially strip all the semantics from it and its required descendant elements.That won’t address a table that does not linearize well, however. This 2018 tutorial from WebAIM does a good job of explaining the table linearization challenge.
Leave a Comment or Response