I was working on an app containing a HTML table that I wanted to make usable with the keyboard. That is, I wanted the user to have the ability to tab into the table, and then use the arrow keys to navigate the table. Many table libraries handle this out of the box in more sophisticated way, but I needed a headless solution for my use case.
The example here is a TypeScript/React one, but the principles apply more broadly.
Making the cells focusable
The first step was to make each table cell (td
) focuable, so that the user could tab into the table. I did this by adding the tabindex
attribute to each cell like so:
<table>
<tr>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
</tr>
<tr>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
</tr>
</table>
And here's the result:
Grid with tab index
Now, you can tab into the table, but still can't navigate across is with a keyboard.
Navigating the table with the arrow keys
The next step is to make the arrow keys work. For this, we can add a keydown
event listener to the table, and then using the event.key
property to determine which key was pressed. Once we've identified the pressed key, we can select the adjacent cell and focus it. Here's the code:
const handleKeyDown = (event: React.KeyboardEvent<HTMLTableElement>) => {
const currentCell = event.target as HTMLTableCellElement;
const table = currentCell.offsetParent as HTMLTableElement | null;
const currentCellParentRow = currentCell.parentNode as HTMLTableRowElement | null;
if (!table || !currentCellParentRow) return;
let cellToFocus: HTMLTableCellElement | null = null;
if (event.code == 'ArrowLeft') {
cellToFocus = table.rows[currentCellParentRow.rowIndex].cells[currentCell?.cellIndex - 1];
cellToFocus?.focus();
} else if (event.code == 'ArrowRight') {
cellToFocus = table.rows[currentCellParentRow.rowIndex].cells[currentCell?.cellIndex + 1];
cellToFocus?.focus();
} else if (event.code == 'ArrowUp') {
cellToFocus = table.rows[currentCellParentRow.rowIndex - 1].cells[currentCell?.cellIndex];
cellToFocus?.focus();
} else if (event.code == 'ArrowDown') {
cellToFocus = table.rows[currentCellParentRow.rowIndex + 1].cells[currentCell?.cellIndex];
cellToFocus?.focus();
} else if (event.code == 'Escape') {
currentCell.blur();
}
};
const KeyboardGridWithNav = () => {
return (
<table onKeyDown={handleKeyDown}>
<tr>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
</tr>
<tr>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
<td tabIndex={0}></td>
</tr>
</table>
);
};
And here's the result:
Grid with tab index and keyboard navigation
With a larger grid or table, you might prefer an escape hatch to focus out of the table. For this, the event handler for the Escape
key, which will blur (i.e., unfocus) the current cell. The example can be extended to do more, such as triggering a callback on hiting the Enter
key.
And that's it. We now have an accessible and type safe way to navigate a HTML table with the keyboard.