Photo by No Revisions on Unsplash
Did you ever try to navigate a webpage using just your keyboard? Do you know how it works under the hood? Do you know what is "tabindex",
and what is its role in keyboard navigation? In this article, we will cover all these burning questions and also learn to implement custom keyboard navigation for a grid using the roving tabindex technique.
Introduction
Recently I was looking to implement keyboard navigation for my daily puzzle game GoldRoad. I had a vague idea about adding key event listeners and using these key presses but I wanted to learn if there is something more to it. And I wasn't disappointed. This article tells the story of the rabbit hole I fell into.
To navigate a website using the keyboard, we typically use the Tab
key (If you're on the Safari browser, and haven't changed any settings then you need to use the option + Tab
keys). We keep pressing the Tab
key and we're taken to different elements of the webpage.
If you're on a laptop or desktop right now, and if you haven't tried it already, you can experience it yourself by pressing the Tab
key a couple of times. The elements that get focussed and in what order are decided by their "tabindex"
attribute.
What is tabindex?
As the name suggests, tabindex
is the index of tabbing and it is a global HTML attribute. It decides which elements on a page get focussed and in what order when keyboard navigation is done using the Tab
key. It can take integer values: a lower positive value implies that the element will be reached (and focussed) ahead of others (including elements having tabindex
of 0). Any negative value (usually -1
is used) means that the element can't be reached by pressing the tab key alone.
Some interactive HTML elements like buttons, inputs, selects, anchor tags etc get a default tabindex
value of 0, and these are the elements that usually get focussed when we do keyboard navigation.
Run the below CodeSandBox to see it in action. The h2
element has a tabindex
of 0
, and since it is present before the grid of buttons, it gets focused first when you press the Tab
key, and then the buttons get focused one after another in the order they were added to the DOM, and so on. Notice that the button with tabindex -1 doesn't get focussed.
Shortcomings of the Tab key
If you tried navigating the previous grid, you'll notice the below problems
To get to the 9th button, 10 key presses are needed (one for the
h2
element, and then 9 more for the 9 buttons). And how do you go back and forth between these buttons? We can keep pressing theTab
key but that is not an optimal experience.What if we wanted to start from the middle of the grid? We could give the middle button a positive
tabindex
value, but even though positive integers are valid values we should avoid using values greater than 0 (this is because it messes up the keyboard navigation for people using assistive technologies).Similar to issue 1, if we've other focusable elements below the grid (e.g. the bottom
p
tag), then it will take a lot of key presses to reach there. How do we solve this?
Grid Navigation using the Arrow Keys
We can improve our grid navigation by using the arrow keys. This will solve the 1st issue, and maybe the 3rd issue partially, but to solve it effectively we need to consider the grid as a single entity in terms of the tab stops. So the first tab key takes us to the h2 element, the second one takes us to the grid, and the third tab press takes us to the paragraph element. And to navigate inside the grid we use the arrow keys with the help of the roving tabindex technique.
What is Roving tabindex?
To consider the grid as a single entity we need to assign a tabindex
of -1 to all the inner buttons and give a tabindex="0"
to that one button where the focus should land. Then we use EventListeners
for tracking the key presses, and we keep roving this tabindex="0"
and the corresponding focus to the appropriate button. At a time there will be only one button having a 0 tabindex.
This technique of managing focus inside a component using the tabindex attribute is called the roving tabindex technique. This also allows us to come back to the same element of the grid from where we tab away from it (this is a requirement for better accessibility).
Roving tabindex implementation
Enough talk. Let's implement the "roving tabindex" technique for the below grid. You can try navigating this grid with your arrow keys after pressing the Tab
key once.
Create a Grid of Buttons
Create a new react project with the create-react-app command or use whichever CRA alternative you prefer. Then create a new file called GridNavigator.js
inside your src
folder. Add the following code to the file.
import { useRef } from "react";
export const GridNavigator = ({ rows, cols, start }) => {
const currentIdx = useRef(start);
return (
<div>
{Array.from(Array(rows), (_, row) => (
<div key={`row-${row}`}>
{Array.from(Array(cols), (_, col) => {
const idx = `${row}${col}`;
return (
<button
key={`col-${idx}`}
tabIndex={currentIdx.current === idx ? "0" : "-1"}
>
{idx}
</button>
);
})}
</div>
))}
</div>
);
};
This component takes the number of rows & columns from its parent and creates a corresponding grid of buttons. It also sets the tabindex
of each of the buttons to -1 (except the button having its idx === start
).
To test this GridNavigator
you can call it inside your App.js
file as shown below
import { GridNavigator } from './GridNavigator';
import './App.css';
function App() {
return (
<div className="App">
<GridNavigator rows={5} cols={5} start='24' />
</div>
);
}
export default App;
Setting the Accessibility Attributes
It is a good practice to assign proper roles to each grid item for better accessibility. And you should also assign proper ARIA attributes to describe the grid to the users who take the help of assistive technologies. Below are some of the attributes we'll be using
role
: Since our grid is made up of interactive elements we'll be usingrole="grid"
for the outermost div,role="row"
for each row, androle="gridcell"
for the buttons. Other possible replacements for thegrid
role is thetable
or thetreegrid
roles.aria-label
: This can be used to give the grid a proper caption.aria-rowcount
&aria-colcount
: For mentioning the rows & columns count of the grid. These attributes need to be used on the outermost div having therole
="grid".
aria-rowindex
&aria-colindex
: On each button having the role ofgridcell
, we should mention its row & column index.
For more details about various aria attributes related to the grid role, you can visit this MDN link.
Make minor adjustments to the code from the last section as shown below
//... rest of the code
return (
<div
role="grid"
aria-label='A grid of buttons'
aria-rowcount={rows}
aria-colcount={cols}
>
{Array.from(Array(rows), (_, row) => (
<div key={`row-${row}`} role="row">
{Array.from(Array(cols), (_, col) => {
const idx = `${row}${col}`;
return (
<button
key={`col-${idx}`}
tabIndex={currentIdx.current === idx ? "0" : "-1"}
role="gridcell"
aria-rowindex={row}
aria-colindex={col}
>
{idx}
</button>
);
})}
</div>
))}
</div>
);
//... rest of the code
Adding the key event listener
Add a key-down event listener on the outermost div. Apart from the event listener, we will give every button a ref so that we can use it to focus the appropriate button on arrow key presses.
Make the following changes to the GridNavigator
component
// import createRef
import { useRef, createRef } from 'react';
Inside the GridNavigator
function, create a ref
to store the references of all the grid buttons
const btnRefs = useRef({});
Add the key event listener on the div with role="grid"
. Also, create and add a ref to all the buttons
return (
<div
role='grid'
aria-label='A grid of buttons'
aria-rowcount={rows}
aria-colcount={cols}
onKeyDown={onKeyDown}
>
{Array.from(Array(rows), (_, row) => (
<div key={`row-${row}`} role='row'>
{Array.from(Array(cols), (_, col) => {
const idx = `${row}${col}`;
// If we don't have a ref for this button, create it
if (!btnRefs.current[idx]) {
btnRefs.current[idx] = createRef();
}
return (
<button
key={`col-${idx}`}
ref={btnRefs.current[idx]}
tabIndex={currentIdx.current === idx ? '0' : '-1'}
role='gridcell'
aria-rowindex={row}
aria-colindex={col}
>
{idx}
</button>
);
})}
</div>
))}
</div>
);
Add the onKeyDown
function along with the helper functions
// Parses the row & col value of the currently selected button
const parseRowCol = () => {
const row = parseInt(currentIdx.current[0], 10);
const col = parseInt(currentIdx.current[1], 10);
return { row, col };
};
// Moves the focus & saves the id of the newly focused button
const handleFocus = (row, col) => {
currentIdx.current = `${row}${col}`;
const btnRef = btnRefs.current[currentIdx.current];
btnRef.current.focus();
};
// Handles keyboard events
const onKeyDown = (event) => {
const { row, col } = parseRowCol();
switch (event.key) {
case 'ArrowUp':
if (row > 0) {
handleFocus(row - 1, col);
}
break;
case 'ArrowDown':
if (row < rows - 1) {
handleFocus(row + 1, col);
}
break;
case 'ArrowLeft':
// If we're on the leftmost col then move to the extreme right
// col of the previous row, provided it's not the first row
if (col > 0) {
handleFocus(row, col - 1);
} else if (row > 0) {
handleFocus(row - 1, cols - 1);
}
break;
case 'ArrowRight':
// If we're on the rightmost col then move to the first
// col of the next row, provided it's not the last row
if (col < cols - 1) {
handleFocus(row, col + 1);
} else if (row < rows - 1) {
handleFocus(row + 1, 0);
}
break;
default:
return;
}
};
Now try navigating the grid using your keyboard. To start the navigation you need to press the Tab
key which will put the focus on the button having idx === start
. Afterwards, you can use your arrow keys to navigate the grid. To come out of the grid press the Tab
key once more. If you try to focus the grid once more, your focus should land on the exact button where you left it.
This implementation solves all the problems we set out to solve. But some of the end users may not know that they can use the arrow keys to navigate the grid, so we mustn't rely on keyboard navigation alone.
Conclusion
Keyboard navigation is crucial for web accessibility and improves the user experience for those who depend on it.
The roving tabindex method can enhance keyboard navigation for complicated interactive elements such as grids.
Combining tabindex with correct accessibility attributes such as roles and ARIA attributes ensures that web applications are accessible to a broader audience, including those with disabilities.
Hope you enjoyed reading the article. If you found any mistake in the article please let me know in the comments so that I can fix it.
Cheers :-)
Top comments (0)