DEV Community

Cover image for Building Accessible Grids 1
Tryggvi Gylfason
Tryggvi Gylfason

Posted on • Updated on

Building Accessible Grids 1

  • Interactive Element: Any html element that can receive focus. For example the a, button and input elements are interactive by default. Any element can be made interactive by adding a tabindex="0 attribute to it. Similarly, any element can be made non-interactive by using tabindex="-1".

Part 2:


Background

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.

Using a <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 <div>s.

  • 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

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

Aria attribute DOM based way

<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>
Enter fullscreen mode Exit fullscreen mode

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 role="grid" attribute.
    • 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)
  • Add rows to the grid
    • Give row role="row"
    • Give row the correct aria-rowindex (starts with 1)
  • 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-colindex attribute (starts with 1)
  • Make sure all elements between grid -> row -> gridcell that 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 tabindex="-1" attribute
    • 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.

Roving tabindex

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="0" -> tabindex="-1" on the currently focused element
  • tabindex="-1" -> tabindex="0" on the next element to focus
  • call 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.

Moving focus to the next element

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 a and button elements.

Wrapping up

As long as

  • This hierarchy of selectors is in place
[role="grid"]
└── [aria-rowindex]
    └── [aria-colindex]
        └── a, button
Enter fullscreen mode Exit fullscreen mode
  • aria-rowindex/aria-colindex are 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.

Part 2:


Top comments (0)