Keyboard navigation
The grid is built to be driven without a mouse: one focused cell, arrows to move, a handful of spreadsheet keys, and menus that are ordinary shadcn menus. This page is the complete list.
| Chloe Tanaka | Sales | Invited | Free | 1 | $0.00 | Sep 17, 2024 | |
| Nia Silva | Support | Invited | Pro | 2 | $24.00 | Jun 27, 2023 | |
| Zara Silva | Growth | Paused | Pro | 3 | $36.00 | Jun 10, 2026 | |
| Elias Müller | Data | Churned | Pro | 2 | $0.00 | Feb 3, 2024 | |
| Leila Kim | Platform | Paused | Enterprise | 48 | $3,792.00 | Mar 24, 2023 | |
| Mia Nguyen | Data | Churned | Free | 1 | $0.00 | Oct 30, 2024 | |
| Sofia Prabhakaran | Sales | Invited | Team | 20 | $580.00 | Aug 27, 2026 | |
| Nia Iyer | Sales | Active | Pro | 4 | $48.00 | Feb 2, 2023 |
Click a cell, then try:↑ ↓ ← → moveShift + ↑ ↓ ← → extend the rangeHome / End first / last columnPageUp / PageDown a screen of rowsEnter or F2 editSpace select the row⌘A select every cell⌘C copyEsc collapse the range
import * as React from "react";
import { DataGrid, createDataGridColumns, useDataGrid } from "@/components/ui/data-grid";
import { makePeople, PLAN_OPTIONS, STATUS_OPTIONS, type Person,} from "@/lib/data-grid-demo-data";
const col = createDataGridColumns<Person>();
const columns = col.columns([ col.select(), col.text("name", { header: "Name", editable: true }), col.text("team", { header: "Team" }), col.enum("status", { header: "Status", options: STATUS_OPTIONS }), col.enum("plan", { header: "Plan", options: PLAN_OPTIONS, editable: true }), col.number("seats", { header: "Seats", editable: { min: 1, step: 1 } }), col.currency("mrr", { header: "MRR", currency: "USD" }), col.date("joinedAt", { header: "Joined" }),]);
const KEYS: Array<[keys: string, what: string]> = [ ["↑ ↓ ← →", "move"], ["Shift + ↑ ↓ ← →", "extend the range"], ["Home / End", "first / last column"], ["PageUp / PageDown", "a screen of rows"], ["Enter or F2", "edit"], ["Space", "select the row"], ["⌘A", "select every cell"], ["⌘C", "copy"], ["Esc", "collapse the range"],];
/** A plain grid to try the keyboard model on: click a cell, then use the keys below. */export default function DataGridKeyboard() { const [data, setData] = React.useState(() => makePeople(50, 33)); const grid = useDataGrid<Person>({ name: "keyboard", data, columns, getRowId: (r) => r.id, pagination: { pageSize: 8 }, onCellEdit: ({ rowId, columnId, value }) => setData((prev) => prev.map((p) => (p.id === rowId ? { ...p, [columnId]: value } : p))), }); return ( <div className="flex flex-col gap-3"> <DataGrid grid={grid} /> <p className="flex flex-wrap items-center gap-x-3 gap-y-1.5 text-sm text-muted-foreground"> <span>Click a cell, then try:</span> {KEYS.map(([keys, what]) => ( <span key={keys} className="inline-flex items-center gap-1"> <kbd className="rounded border bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground"> {keys} </kbd>{" "} {what} </span> ))} </p> </div> );}Getting in
Section titled “Getting in”Tab reaches the toolbar, then the column headers (each sortable header is a button), then the body, then the pager. The body uses a roving tabindex: only the focused cell is a tab stop, and until a cell has been focused there is none. The first time, press Tab to a header and then any arrow key, or click a cell; after that Tab returns to the last focused cell.
⌘ below is Ctrl on Windows and Linux.
In the body
Section titled “In the body”| Keys | What happens |
|---|---|
| ↑ ↓ ← → | Move the focused cell. The range collapses to it. |
| Shift+↑ ↓ ← → | Extend the range one cell, keeping its anchor. |
| Home / End | First / last data column in the current row. |
| ⌘Home / ⌘End | First column of the first row / last column of the last row on the current page. |
| PageUp / PageDown | Up / down by one screen of rows (viewport height ÷ row height, minus one), within the current page. |
| Enter or F2 | Open the editor on an editable cell; on an expandable row with no editor, toggle it. |
| Space | Toggle the focused row’s selection (with its sub-rows). Announces the count. |
| ⌘A | Select every cell. Announces the count. |
| ⌘C | Copy the selected cells as TSV. Announces “Copied n cells”. |
| Esc | Collapse a range to the focused cell. In fullscreen with nothing selected, leave fullscreen. |
Movement skips the checkbox, expander, row-number and actions columns at the ends of a row, and scrolls the cell into view, including rows that are not yet rendered in a virtualized grid.
In an editor
Section titled “In an editor”| Keys | What happens |
|---|---|
| Enter | Commit and move down one cell. |
| Shift+Enter | Commit and stay. In a textarea: insert a line break. |
| Tab / Shift+Tab | Commit and move right / left. |
| Esc | Cancel: close the editor and keep the old value. |
| arrows, Home, End | Belong to the input (move the caret). |
A select editor opens its list at once: arrows move, Enter chooses and commits, Esc cancels. A checkbox commits as soon as it is toggled. See inline editing for what happens on commit.
In the header
Section titled “In the header”| Keys | What happens |
|---|---|
| Tab | Focus the next header’s sort button, then its filter and menu buttons (they appear on focus). |
| Enter or Space on the title | Toggle the sort: ascending, descending, off. |
| Shift+Enter on the title, or Shift-click | Add the column to a multi-column sort instead of replacing it. |
| Enter on the ⋯ button | Open the column menu: sort, filter, pin, move, group, fit, hide. |
| Enter on the filter button | Open the filter popover; its inputs take the usual keys. |
| right-click on a header | Open the column menu with the pointer. |
Inside a menu (Base UI): ↑ ↓ move, Enter activates, Esc closes and returns focus to the trigger. Move left and Move right in the column menu are the keyboard route to reordering; dragging a header has no keyboard equivalent.
Toolbar and pager
Section titled “Toolbar and pager”Every control is a button, input or select with its normal keys. In the pager, type a page number in the “Go to page” field and press Enter. The search box clears on Esc.
Focus management
Section titled “Focus management”- After a commit or cancel, focus returns to the cell, or to the cell Enter or Tab moved to.
- The grid only moves focus when focus is already inside it; changing the focused cell from code while you are typing in the search box does not steal it.
- In a virtualized grid, moving to a row that is not rendered scrolls it into view first and focuses it once it exists.
- Opening a menu or popover moves focus into it; closing it returns focus to the trigger.
Announcements
Section titled “Announcements”A visually hidden aria-live="polite" region in the grid root announces, using the
labels:
| Event | Text |
|---|---|
| sort toggled from the header | Name: Sort ascending / … Sort descending / … Clear sort |
| row selected with Space, or ⌘A | labels.selected(n): “3 selected” |
| cells or rows copied | labels.copied(n): “Copied 12 cells” |
Call useDataGridUi().announce(message) from your own parts to speak through the same region.
See also
Section titled “See also”- Cell selection and clipboard
- Inline editing
- Accessibility: roles, attributes and the known gaps.