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.


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++) {
    var allRowGroups = document.querySelectorAll('thead, tbody, tfoot');
    for (var i = 0; i < allRowGroups.length; i++) {
    var allRows = document.querySelectorAll('tr');
    for (var i = 0; i < allRows.length; i++) {
    var allCells = document.querySelectorAll('td');
    for (var i = 0; i < allCells.length; i++) {
    var allHeaders = document.querySelectorAll('th');
    for (var i = 0; i < allHeaders.length; i++) {
    // this accounts for scoped row headers
    var allRowHeaders = document.querySelectorAll('th[scope=row]');
    for (var i = 0; i < allRowHeaders.length; i++) {
    // caption role not needed as it is not a real role and
    // browsers do not dump their own role with display block
  } catch (e) {
    console.log("AddTableARIA(): " + e);



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 {
    // there is no ARIA role for description lists
    var allLists = document.querySelectorAll("ol, ul, dl");
    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 allDefTerms = document.querySelectorAll("dt");
    for (var i = 0; i < allDefTerms.length; i++) {
      allDefTerms[i].setAttribute("role", "term listitem");
    var allDefItems = document.querySelectorAll("dd");
    for (var i = 0; i < allDefItems.length; i++) {
      allDefItems[i].setAttribute("role", "definition listitem");
  } catch (e) {
    console.log("AddListARIA(): " + e);



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.

Each video uses NVDA 2018.1.1 and Firefox 59.0.3. The first video demonstrates the table in a viewport wide enough to allow navigation via NVDA table commands (Ctrl + Alt + ///). The second video demonstrates how Firefox no longer treats it as a table when the responsive styles kick in and table navigation commands no longer work.

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.

With the addition of the ARIA, Firefox retains the table semantics and I am able to navigate the table as a table in NVDA.

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.



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!

Nick Beranek; . Permalink
In response to Nick Beranek. Reply

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

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!

Nick Beranek; . Permalink
In response to Nick Beranek. Reply

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.

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>

This site uses Akismet to reduce spam. Learn how your comment data is processed.

To respond on your own website, enter the URL of your response which should contain a link to this post's permalink URL. Your response will then appear (possibly after moderation) on this page. Want to update or remove your response? Update or delete your post and re-enter your post's URL again. (Learn More)