
table-nav is a headless TypeScript utility that handles keyboard focus navigation across HTML tables and data grids.
You can use Arrow keys to move between cells, while Home, End, PageUp, PageDown, CTRL shortcuts, Enter, and ESC handle common grid navigation tasks.
The framework-agnostic @table-nav/core package works with existing DOM markup and has no runtime dependencies. React projects can use @table-nav/react, which wraps the same DataGridNav class in a useTableNav() hook.
How to Use It:
Installation
Install the core package for JavaScript or other framework integrations.
yarn add @table-nav/core
React projects use both packages.
yarn add @table-nav/core @table-nav/react
Basic Usage
table-nav moves focus between existing cells. Each destination cell therefore needs to accept programmatic focus. A typical grid pattern places one cell in the normal tab sequence and assigns tabindex="-1" to the other cells.
<table id="orders-grid" role="grid" aria-label="Recent orders">
<thead>
<tr>
<th tabindex="-1">Order</th>
<th tabindex="-1">Customer</th>
<th tabindex="-1">Status</th>
</tr>
</thead>
<tbody>
<tr>
<td tabindex="0">A-1042</td>
<td tabindex="-1">Morgan Lee</td>
<td tabindex="-1">Processing</td>
</tr>
<tr>
<td tabindex="-1">A-1043</td>
<td tabindex="-1">Taylor Kim</td>
<td tabindex="-1">Shipped</td>
</tr>
</tbody>
</table>
Create DataGridNav and attach both keyboard handlers to the table. The keyup handler clears the stored key sequence used for multi-key shortcuts.
import { DataGridNav } from '@table-nav/core';
const table = document.querySelector('#orders-grid');
const tableNav = new DataGridNav();
table.addEventListener('keydown', tableNav.tableKeyDown);
table.addEventListener('keyup', tableNav.tableKeyUp);
Keyboard Shortcuts
| Key | Behavior |
|---|---|
ArrowRight | Moves focus to the next cell in the row. |
ArrowLeft | Moves focus to the previous cell in the row. |
ArrowDown | Moves focus to the same column in the next row. |
ArrowUp | Moves focus to the same column in the previous row. |
Home | Moves focus to the first cell in the current row. |
End | Moves focus to the last cell in the current row. |
PageUp | Moves toward the beginning of the row group. |
PageDown | Moves toward the end of the row group. |
Control + Home | Moves to the first cell of the first row. |
Control + End | Moves to the last cell of the last row. |
Enter | Focuses the first matching interactive element inside the cell. |
Escape | Moves focus from an element inside the cell back to its cell. |
Set pageUpDown when PageUp and PageDown should move a fixed number of rows. The default behavior targets the first or last row in the current sibling set.
const tableNav = new DataGridNav({
pageUpDown: 8
});
Configuration Options
debug(boolean): Prints navigation messages to the browser console. Defaults tofalse.pageUpDown(number): Sets the row distance for PageUp and PageDown. An unset value targets the first or last row.selectors(object): Replaces selected DOM selectors used for cells, rows, row groups, and focusable elements.
The built-in selectors cover native table elements and common ARIA grid roles:
const tableNav = new DataGridNav({
selectors: {
Cell:
'[role="cell"],[role="gridcell"],[role="columnheader"],[role="rowheader"],td,th',
Row:
'[role="row"],tr',
RowGroup:
'[role="rowgroup"],thead,tbody,tfoot',
Focusable:
'a,frame,iframe,input:not([type=hidden]):not(:disabled),' +
'select:not(:disabled),textarea:not(:disabled),' +
'button:not([aria-disabled="true"]):not([tabindex="-1"]):not(:disabled),' +
'*[tabindex]'
}
});
A partial selectors object keeps the remaining defaults. Custom markup only needs entries for selectors that differ from the built-in structure.
const tableNav = new DataGridNav({
selectors: {
Cell: '.data-cell',
Row: '.data-row'
}
});
Interactive Elements Inside Cells
Enter transfers focus from a cell to the first element that matches the Focusable selector. Custom interactive elements need a matching selector or an explicit tabindex.
Once focus sits inside a cell, table-nav uses a different keyboard path:
Escapereturns focus to the containing cell.ArrowRightandArrowDownmove to the next matching element in the cell.ArrowLeftandArrowUpmove to the previous matching element.- Navigation wraps between the first and last matching elements.
Some widgets need arrow keys for their own controls. disable() pauses table-nav keyboard processing for those situations. Restore it with enable() when the widget finishes its interaction.
const slider = document.querySelector('#volume');
slider.addEventListener('focus', function () {
tableNav.disable();
});
slider.addEventListener('blur', function () {
tableNav.enable();
});
React Integration
@table-nav/react exposes the same navigation logic through useTableNav().
import { useTableNav } from '@table-nav/react';
function OrdersTable() {
const { tableNav, listeners } = useTableNav({
pageUpDown: 6
});
return (
<table
role="grid"
aria-label="Orders"
{...listeners}
>
{/* rows and cells */}
</table>
);
}
The hook returns:
listeners: ReactonKeyDownandonKeyUphandlers for the grid element.tableNav: TheDataGridNavinstance used by those handlers.
The instance remains available for calls such as disable() and enable() when a cell widget needs control of its own keyboard input.
API Methods
// Handle key presses from the grid. tableNav.tableKeyDown(event); // Clear the stored key sequence after key release. tableNav.tableKeyUp(); // Pause table-nav keyboard processing. tableNav.disable(); // Resume table-nav keyboard processing. tableNav.enable(); // Handle keyboard input from an element inside a cell. tableNav.cellNavigation(event); // Handle keyboard input at the grid-cell level. tableNav.gridNavigation(event);
Alternatives:
- Enable Keyboard Accessibility On HTML Lists – Gridnav.js
- Enhanced Keyboard Navigation in JavaScript – FocusManager
- Lightweight Vanilla JS Data Grid for Modern Web Apps – BWDataTable
FAQs:
Q: Can table-nav work with a plain HTML table?
A: Yes. @table-nav/core works directly with DOM elements. The table cells need suitable focus behavior and the key handlers must be attached to the grid element.
Q: Does table-nav add ARIA roles and tabindex attributes?
A: No. The library handles focus movement. Grid semantics, labels, tab stops, and focus styles remain part of your markup and CSS.
Q: Why do arrow keys stop working correctly inside an input or slider?
A: Both the grid and the control may need the same keys. Pause table-nav for controls that must own their directional keys, then call enable() when the control releases focus.
Q: Can I change the HTML structure used for rows and cells?
A: Yes. Pass custom Cell, Row, RowGroup, or Focusable selectors through the selectors option.







