Data grid
A table people edit cell by cell, with the keyboard, paste and a save state per cell.
Examples
Eval cases2 of 4 pass
| Case | Input | Expected | Pass | Score |
|---|---|---|---|---|
| eval_0411 | I was charged twice for the March invoice, please refund one. | Billing | 0.94 | |
| eval_0412 | The export button spins forever in the old browser. | Bugs | 0.88 | |
| eval_0413 | New teammate cannot see the support-agent project. | Access | 0.41 | |
| eval_0414 | Add the user message | Bugs | 0 to 1 |
editsEsccancelsCcopiesPaste fills from here
Tickets routed today3
| Ticket | Subject | Queue | Priority |
|---|---|---|---|
| TKT-1042 | Refund for duplicate charge | Billing | 2 |
| TKT-1043 | Export spins forever in the old browser | Bugs | 1 |
| TKT-1044 | Invite link expired before the new hire used it | Access | 3 |
Editor
Name refund-check text
Input I was charged twice for March. long-text
Score 0.94 number
Queue Billing select
Pass boolean
States
0.94 rest
0.94 saving
0.94 saved
Not saved: Eval running0.94 error
Invalid: Use 0 to 1.1.4 invalid
0 to 1 empty
0.94 read only
Usage
When to use it
- Use
- People edit many records in place, a cell at a time: eval cases, ticket routing, a pricing sheet.
- Use
- People paste rows from a spreadsheet and expect them to fill cells.
- Use
- Each edit saves on its own and the person needs to see which saved and which failed.
- Not for
- People read, sort and open records but do not edit cells. Use Table.
- Not for
- One record is edited as a form. Use Field.
Rules
9 rules
- Must
- Give each DataGridCell a label that names its row: "Score for eval_0412". Because the editor, the error and every save announcement read the label, so "Score" alone never says which row saved.
- Must
- Set rowCount and colCount to what you render, and index rows from 0 with the header row at -1. Because the arrows clamp to the counts and aria-rowindex comes from index, so a wrong count strands focus or misreads the row.
- Must
- Return the save from onCommit and reject with an Error that says what to do. Because a void onCommit shows no saving or saved mark, so a failed save looks done.
- Must
- Pass cellId from the row id whenever rows can sort or filter. Because save state is keyed by position without it, so after a sort the saving mark lands on another row.
- Never
- Drop a bad value in onCommit; store it and let validate say why. Because validate reads the stored value, so a dropped value vanishes with no message.
- Never
- Window or virtualize the rows of one DataGrid; paginate and pass the page. Because moves, copy and paste reach only mounted cells, so a paste past the window is dropped.
- Should
- Render DataGridHelp next to the table; the grid points aria-describedby at it.
- Should
- Use editor="number" for numbers and mono for IDs, so they right-align and line up.
- Should
- With focusableHeader, put itemProps({row: -1, col}) on a head button and focusable={false} on its DataGridHead.
Accessibility
5 checks
- The table is role="grid" with one tab stop; cells are gridcells with aria-readonly, aria-invalid and aria-busy.
- The active cell shows a 2px ink ring drawn inside it, so a scroll box never clips it.
- Saves, failures, copies and pastes are announced in one polite status line.
- An invalid cell shows "Invalid:" and the reason, a failed save "Not saved:" and the error, as text on the fail wash, not as color alone.
- Only an invalid value sets aria-invalid; a failed save is announced in the status line, since the value itself may be fine.
Keyboard
7 keys
- Arrows, Home, End
- Move a cell, or to the row ends; with Mod, Home and End go to the corners.
- Page Up, Page Down
- Move ten rows.
- Enter, F2 or a character
- Edits the cell; a character starts the text with it.
- Space
- Toggles a boolean cell.
- Delete, Backspace
- Clears the cell.
- Mod+C, Mod+V
- Copies the active cell; pastes tab-separated text right and down from it.
- Escape, Enter, Tab
- In an editor: cancel, save and move down, save and move sideways.
Edge cases
7 edge cases
- A failed save
- Shows "Not saved:" and the error; the value stays, so the person can retry.
- Delete on an empty cell
- Does nothing: no onCommit, no save mark.
- Paste a value a cell already holds
- Leaves it unsaved and announces it apart; a cell whose last save failed is saved again.
- Narrow container
- The table scrolls sideways; DataGridHelp stays unless you hide it.
- Paste over read-only cells or past the last row
- Those cells are skipped; the count is announced.
- Rows sort while a cell saves
- The mark follows cellId; without it, it stays at the position.
- A 400-character value
- The cell stops at 320px and cuts with an ellipsis; copy takes it all.
API
21 props
DataGrid rowCountnumberdefaultrequiredDataGrid colCountnumberdefaultrequiredDataGrid readOnlybooleandefaultfalseDataGrid focusableHeaderbooleandefaultfalseDataGrid activeCell{row: number; col: number}DataGrid onActiveCellChange(cell: CellPosition) => voidDataGridTable aria-labelstringdefaultrequiredDataGridTable size'sm' | 'default' | 'lg'default'sm'DataGridTable title, count, actionsReactNodeDataGridRow indexnumberdefaultrequiredDataGridHead colnumberdefaultrequiredDataGridCell row, colnumberdefaultrequiredDataGridCell labelstringdefaultrequiredDataGridCell valuestring | number | boolean | nulldefaultnullDataGridCell editor'text' | 'long-text' | 'number' | 'select' | 'boolean'DataGridCell options{value: string; label: string}[]DataGridCell onCommit(value: CellValue) => void | Promise<unknown>DataGridCell validate(value: CellValue) => string | nullDataGridCell cellIdstringdefault'row:col'DataGridCell statusCellSaveStateDataGridCell monobooleandefaultfalse
Tokens
16 tokens
--line-frame- oklch(var(--ink-c) / var(--line-frame-alpha))
--line-row- oklch(var(--ink-c) / 0.08)
--surface-page- var(--paper)
--radius-soft- 6px
--radius-frame- 10px
--ink- oklch(var(--ink-c))
--accent-wash- var(--surface-selected)
--fail-wash- oklch(94.5% 0.027 26)
--text-2- oklch(var(--ink-c) / 0.80)
--text-3- oklch(var(--ink-c) / 0.66)
--type-body- 340 14px/20px var(--font-sans)
--type-body-sm- 380 13px/18px var(--font-sans)
--type-tag- var(--type-code-sm)
--row-sm- 32px
--features-data- "tnum", "zero", "ss04"
--dur-spin- 800ms