table-nav: Accessible Keyboard Navigation for Data Grids

Category: Javascript | August 21, 2026
Authorkonsalex
Last UpdateAugust 21, 2026
LicenseMIT
Views0 views
table-nav: Accessible Keyboard Navigation for Data Grids

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

KeyBehavior
ArrowRightMoves focus to the next cell in the row.
ArrowLeftMoves focus to the previous cell in the row.
ArrowDownMoves focus to the same column in the next row.
ArrowUpMoves focus to the same column in the previous row.
HomeMoves focus to the first cell in the current row.
EndMoves focus to the last cell in the current row.
PageUpMoves toward the beginning of the row group.
PageDownMoves toward the end of the row group.
Control + HomeMoves to the first cell of the first row.
Control + EndMoves to the last cell of the last row.
EnterFocuses the first matching interactive element inside the cell.
EscapeMoves 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 to false.
  • 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:

  • Escape returns focus to the containing cell.
  • ArrowRight and ArrowDown move to the next matching element in the cell.
  • ArrowLeft and ArrowUp move 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: React onKeyDown and onKeyUp handlers for the grid element.
  • tableNav: The DataGridNav instance 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:

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.

You Might Be Interested In:


Leave a Reply