Skip to main content

Roving Tabindex

An accessibility pattern for a grouped set of elements


How to
  • Use the tab key to navigate to the Menu.
  • Then, use the ArrowUp and ArrowDown keys to go through each option.
  • Try pressing Home or End to jump right to the first or last elements on the group.
  • Press tab (or shift+tab) again to exit.

Development Instructions

1. Wrap each roving tabindex group in a RoverProvider

You can nest roving tabindex components in other DOM elements or React components.

import { RoverProvider } from "@jtmdias/react-a11y-tools";
...
<RoverProvider>
{..content here}
</RoverProvider>

You can also choose the direction of the navigation. It can either be "vertical" (default) or "horizontal".

import { RoverProvider } from "@jtmdias/react-a11y-tools";
...
<RoverProvider options={{
direction: "horizontal"
}}>
{..content here}
</RoverProvider>

Choosing between "vertical" and "horizontal" implies you can use:

  • horizontal - "ArrowLeft" and "ArrowRight" keys
  • vertical - "ArrowUp" and "ArrowDown"
  • Home key to go to the first element
  • End key to go to the last element

2. Wrap each focusable element

For composition, try to identify which elements are the ones that are going to be affected by the RoverProvider. For each one of those, wrap them with your own component and use the useRover and useFocus hooks.

import { useRover, useFocus } from "@jtmdias/react-a11y-tools";
...
const MenuButton = ({ disabled = false, children }) => {
const buttonRef = useRef(null);

const [tabIndex, focused, handleKeyDown, handleClick] = useRover(buttonRef, { disabled });

useFocus(focused, buttonRef);

function onKeyDown(event) {
handleKeyDown(event);
yourOwnFunctionToDoWhatever(event);
}

function onClick(event) {
handleClick(event);
yourOwnFunctionToDoWhatever(event);
}

return (
<button
ref={ref}
type="button"
tabIndex={tabIndex}
disabled={disabled}
onKeyDown={handleKeyDown}
onClick={onClick}
>
{children}
</button>
);
};

3. Place them inside your RoverProvider structure

Since RovingTabIndex relies on React Context, there's no need to have the buttons all as direct children of the provider. You can nest as deep as you'd like and it will work as well 🙂

import { RoverProvider } from "@jtmdias/react-a11y-tools";
...
<RoverProvider options={{
direction: "horizontal"
}}>
<MenuButton>First Button</MenuButton>
<MenuButton disabled>Second Button</MenuButton>
<ul>
<li><MenuButton>Another Button</MenuButton>
<li><MenuButton>Another Button</MenuButton>
<li><MenuButton>Another Button</MenuButton>
<li><MenuButton>Another Button</MenuButton>
</ul>
</RoverProvider>

Extra Features

1. Disable the "loop around" feature

By default, if you try to tab past the very start or very end of the roving tabindex then tabbing does not wrap around. The RoverProvider has an optional loopAround property on the options prop that allows you to change this:

import { RoverProvider } from "@jtmdias/react-a11y-tools";
...
<RoverProvider options={{
loopAround: false, // default is true
direction: "horizontal"
}}>
...
</RoverProvider>

If this option is set to true then tabbing will wrap around if you reach the very start or very end of the roving tabindex items, rather than stopping. Note that this option does not apply if the roving tabindex is being used with a grid.

2. Disable the "focus on click" feature

By default, clicking on a roving tabindex item will result in focus() being invoked on the item (via useFocus). So, if you want to when you invoke focus() ONLY when you use the keyboard to move to an item, then the RoverProvider has an optional focusOnClick property on the options prop that allows you to change this:

import { RoverProvider } from "@jtmdias/react-a11y-tools";
...
<RoverProvider options={{
focusOnClick: false, // default is true
direction: "horizontal"
}}>
...
</RoverProvider>

Grid Usage

This package supports a roving tabindex in a grid. For each usage of the useRover hook in the grid, you must pass a row index value as a property in the second argument to the hook:

import { useRover } from "@jtmdias/react-a11y-tools";
...
const [tabIndex, focused, handleKeyDown, handleClick] = useRover(
ref,
{
disabled,
rowIndex: yourRowIndexValueHere
},
);

The row index value must be the zero-based row index for the grid item that the hook is being used with. Thus all items that represent the first row of grid items should have passed to the hook, the second row 1, and so on. If the shape of the grid can change dynamically then it is fine to update the row index value. For example, the grid might initially have four items per row but get updated to have three items per row.

The direction property of the RoverProvider is ignored when row indexes are provided. This is because the ArrowUp and ArrowDown keys are always used to move between rows.

Differences with Focus Manager

Both are made to deal with focus and navigation but do it in different ways and for different scenarios:

  1. The FocusManager can, if necessary, scope the focus inside a group and allows the user to press the tab key to move to the next "tabbable" element (or shift+tab to move to the previous).

    💡 Tip: Use it for dealing with interface elements that require focus control, like popovers, modals and side menus.


  1. The RoverProvider can also manage focus inside a group but, instead of using the tab key to move back and forth between elements, it uses navigation keys (ArrowUp, ArrowDown, ArrowLeft or ArrowRight) to do that.

    It also makes the other group's elements unable to receive focus using the tab key since it applies the negative tabindex to the components that are not currently selected.

    💡 Tip: Use it for dealing with interface elements that require selection, like a custom select element or a navigation menu. (Still, you should try to use the native HTML elements).

Props

Rover Provider