Table with Expando Rows
I regularly work on projects with HTML tables that have been pushed to the edge with styles, scripts, and widget features. A common pattern is where rows are hidden until the user opts to show them. Unfortunately, the pattern is often over-complicated with unnecessary script and styles that regularly break the table semantics or fail to work across all contexts.
Typically there are only two things you as an author need to do for a row expando to retain table semantics (other than using a contiguous <table>
, of course):
- Make sure you use the
table-row
CSSdisplay
property instead ofblock
, and - Use a disclosure widget as your toggle.
Example
You can visit the pen directly to fiddle with it, or view in debug mode to test it in your favorite AT (assistive technology).
Note this example is not responsive.
See the Pen Table with Expando Rows by Adrian Roselli (@aardrian) on CodePen.
Disclosure Widget
The disclosure widget is a native HTML <button>
. If you read my post Link + Disclosure Widget Navigation, it goes into more detail than I will here.
Much like that example, the accessible name is concatenated from its own aria-label
value and the text value in the relevant cell to provide a more natural name (eg: [n] more items from [author]
). The aria-expanded
value tells assistive technology whether the button has been expanded or not, and is also the hook we use for the styling.
[…] <tr> <td> <button type="button" id="btnMSb" aria-expanded="false" onclick="toggle(this.id,'#MS01b,#MS02b,#MS03b');" aria-controls="MS01b MS02b MS03b" aria-label="3 more from" aria-labelledby="btnMSb lblMSb"> […] </button> </td> <td id="lblMSb">Mary Shelley</td> […] </tr> […]
The aria-controls
value is a space-separated list of the id
s of the rows you are affecting. Where aria-controls
is supported, confirm that its announcement in screen readers (JAWS only today) is not too verbose for your users. If it is, you may want to remove it altogether. Its absence won’t result in a barrier, but it may make the widget easier to use for some users when useful screen reader support starts to appear.
The CSS
The easiest way to ensure the programmatic state of the button matches the visual styles is to use attribute selectors such as button[aria-expanded="true|false"]
. The styles for both example tables only change the SVG.
.cell button[aria-expanded="true"] svg { transform: rotate(90deg); } .row button[aria-expanded="true"] svg { transform: rotate(180deg); }
Your own styles will likely vary, of course.
The Script
The function is nothing special. It takes a list of id
s and feeds them into a query selector, and it takes the id
of the button as well. It checks then flips the aria-expanded
value on the button while swapping the value of the class
between shown
and hidden
.
If I could use adjacent sibling selectors (keying off the aria-expanded
value) to toggle the visibility of the rows I would. The HTML table structure precludes that, which is the only reason I am using a class to do the work.
This is not production-ready script. It does, however, take Internet Explorer into account by not using classList.replace
, which IE does not support.
function toggle(btnID, eIDs) { // Feed the list of ids as a selector var theRows = document.querySelectorAll(eIDs); // Get the button that triggered this var theButton = document.getElementById(btnID); // If the button is not expanded... if (theButton.getAttribute("aria-expanded") == "false") { // Loop through the rows and show them for (var i = 0; i < theRows.length; i++) { theRows[i].classList.add("shown"); theRows[i].classList.remove("hidden"); } // Now set the button to expanded theButton.setAttribute("aria-expanded", "true"); // Otherwise button is not expanded... } else { // Loop through the rows and hide them for (var i = 0; i < theRows.length; i++) { theRows[i].classList.add("hidden"); theRows[i].classList.remove("shown"); } // Now set the button to collapsed theButton.setAttribute("aria-expanded", "false"); } }
At page load, the rows in my example are not hidden with inline CSS. Whether you hide or display them on initial load in the name of Progressive Enhancement is up to you and your use case. Either one can be a valid approach, but account for it in your function as well.
Tables
I made two tables so you could see two ways this might work. The full-row disclosure widget shows the text that is also announced to screen readers, and it provides a much larger hit area. As a column-spanning cell it can complicate table navigation for novice screen reader users but is much easier to find — you can stumble across it from any column.
The other example has the disclosure widget in its own column, arguably making it easier to avoid. It also warrants its own column header. The value is Toggle
and I use a well-tested technique to visually hide it (partly because NVDA does not support the abbr
attribute on <th>
). See the visually-hidden
class to steal the styles.
The HTML for the row is not complex. Just an id
and a class
, with the latter toggled via script.
<tr id="EDENS02b" class="hidden"> <td></td> <td>Emma Dorothy Eliza Nevitte Southworth</td> <td>Unknown; or the Mystery of Raven Rocks</td> <td>1889</td> <td></td> <td></td> </tr>
The CSS
The CSS for the two classes that adjust the display
of the row is critical. If you use display: block
instead of display: table-row
then the browser drops all the semantics for the row and assistive technology cannot navigate it. See my post Tables, CSS Display Properties, and ARIA for more detail.
tr.shown, tr.hidden { background-color: #eee; display: table-row; } tr.hidden { display: none; }
Screen Readers
Note that while a screen reader will not announce how many new rows are added (hence the accessible name to manage that expectation), once the new rows are visible the screen reader factors them into the total row count and user’s position within the table.
It is also important to ensure any new rows you add come after the control that makes them appear in the source order. Otherwise a screen reader use cannot be expected to know where they have appeared, let alone navigate around the table looking for them.
I have embedded some videos showing how a screen reader user might navigate the expando feature.
JAWS / IE11
NVDA / Firefox
VoiceOver / Safari
<caption>
disappears when the disclosure is opened (it moves to below the table); I have no idea why and I have not taken time to dive into it.6 Comments
This is good when the data in the expanded row is in the same data structure as the other data; frequently I see expanded rows where its all of the other data for that entry that they couldn’t fit on the table (or in a responsive design where they can’t fit all the columns in the view).
In response to .This pattern cannot fix terrible information architecture. I did, however, get forced to making a table with nested tables hidden behind expando buttons and multiple nested tables with additional content. Both are technically accessible, but still a terrible idea.
Perhaps I’ve misread the release changes in 2019, but hasn’t JAWS essentially dropped support for aria-controls?
“If you encounter an element on a web page with a defined ARIA controls relationship, JAWS will no longer say ‘use JAWSKEY+ALT+M to move to controlled element’ by default. In most cases, the target of the controls relationship is adjacent to the element or does not provide any useful information.”
In response to .Peter, it has (per the 13th bullet in the release notes). You hear
use JAWS Key + ALT + M to move to controlled elementin my video (10 second mark) because I am still running JAWS 2018. If JAWS brings it back and others later support it, I am hoping they do a better job than JAWS did.
Why do we set the
display
value for the.shown
class? It’str
’s default value.
In response to .Great question! I did a poor job of explaining why I am doing it this way because you are right, you do not need to add the
shown
class. Readers can test this by commenting out lines 10 and 20 in the JavaScript and see the widget works fine.However, I used it for two reasons:
- to demonstrate that
display: table-row;
is the value to use overdisplay: block;
if you are messing with the display properties, and- to provide a hook for the background color to make it easier to visually distinguish the now-visible rows.
Obviously if you want simpler code and/or you come up with a better approach that still retains the row semantics, then by all means ignore my code.
Leave a Reply to Adrian Roselli Cancel response