Interactive Element: Any html element that can receive focus. For example the
inputelements are interactive by default. Any element can be made interactive by adding a
tabindex="0attribute to it. Similarly, any element can be made non-interactive by using
Building accessible lists and grids on the web is unfortunately difficult.
In frameworks like React, where a Grid component does not necessarily know about or control its children, it can be even harder.
<table> and associated child elements is the right choice sometimes. Other times it's not. These elements are awkward to lay out and more importantly impose limitations on the DOM structure . More complex grids may need to use
- Grids should be navigable with arrow keys. You can read about the expected keyboard support for data grids in the WAI ARIA practices.
- Grids should be skippable with a single Tab key press. They are composite components. You can read more in this excellent thread Devon Govett@devongovettThere's a very common misconception that pure HTML with no JS is somehow more accessible. Unless you're building a very basic content site with no interactivity, that's just plain false!
Until the platform improves, you need JS to properly implement keyboard navigation.
Thread.18:15 PM - 20 Jun 2020
One way to add keyboard support to grids is to base the navigation on the current state of the DOM instead of the framework state (React and Vue for example).
<div role="grid"> ... <div role="presentation"> ... <div role="row" aria-rowindex="1"> ... <div role="presentation"> ... <div role="gridcell" aria-colindex="1"> ... <a tabindex="-1"></a> <button tabindex="-1"></button> </div> </div> </div> </div> </div>
Let's walk through how this is done on a high level. In the next post we'll go more concrete and actually implement this.
- Create the root grid element
- Give grid the
- Give grid
tab-index="0"so users can navigate to it (remember to add some CSS focus styles. E.g. a border or an outline that indicates the grid is in focus)
- Give grid the
- Add rows to the grid
- Give row
- Give row the correct
aria-rowindex(starts with 1)
- Give row
- Add cells (a row,col combination) to the rows
- give cell either a
role="columnheader"if it's a column header, or a
role="gridcell"if it's a regular cell
- Give cell the correct
aria-colindexattribute (starts with 1)
- give cell either a
- Make sure all elements between
gridcellthat do not have an explicit role, get
role="presentation"added to them.
- This removes those elements from the accessibility tree and ensures screen readers can correctly interpret and navigate the grid.
- Make sure that all interactive elements in the grid have
- This removes them from the natural tab order of the page and ensures that users can jump over the whole grid with a single Tab key press if they want to skip it.
- Add a keydown listener to the root grid element (
role="grid") and respond to key presses like the arrow keys.
- Once an arrow key is pressed we choose what to focus next in the DOM. More on that in a bit.
The DOM based approach gives us the benefit of the focus management being transparent to the grid and its child components. If a new interactive element is added to a cell in the grid it will work without modification.
The downside is frameworks like React can change the DOM at any time. Because of this, the first thing that is done in keydown event handler before moving any focus is ensuring that a focusable element (with
tabindex="0" attribute) actually exists in the grid. If it doesn't exist, fall back to the first interactive element in the grid.
This should not happen much unless your framework is adding/removing cells from the DOM frequently. I have not seen problems with this in a virtually scrolled, sortable/filterable grid that loads more data on scroll so I expect this is will work well in most cases.
Now that we have found the currently focused element in the grid we need to decide which element to focus next. If we find that element we move the focus to that element:
tabindex="-1"on the currently focused element
tabindex="0"on the next element to focus
focus()on the next element.
This technique is called roving tabindex. You can read more about it in the WAI-ARIA Authoring Practices or check out this excellent video from the A11ycasts series on the Google Chrome Developers YouTube channel.
So how do we find the next element to focus in a reliable way? This depends on what the grid actually is. If the grid is a spreadsheet it might make sense to focus the next cell element. But if the grid is a collection of links and buttons structured in a table - for example a playlist on Spotify, then jumping to the next interactive element would make sense.
In the next post I'll go through a concrete implementation keyboard support of a grid containing
As long as
- This hierarchy of selectors is in place
[role="grid"] └── [aria-rowindex] └── [aria-colindex] └── a, button
aria-colindexare 1-based and sequential
Then the keyboard navigation will work. Additional wrapper elements between each layer do not matter.
Building accessible grids and lists can be daunting and too many don't bother. Fortunately adding keyboard support can be done in a generic, framework and use-case agnostic, repeatable way that is likely to survive future changes to the components without breaking or needing modification.
In the next post we'll implement a grid with proper keyboard support using this method.