Data grid

A table people edit cell by cell, with the keyboard, paste and a save state per cell.

Examples

Eval cases

Eval cases2 of 4 pass
CaseInputExpectedPassScore
eval_0411Billing0.94
eval_0412Bugs0.88
eval_0413Access0.41
eval_0414Bugs0 to 1

editscancelscopiesPaste fills from here

Read only

Tickets routed today3
TicketSubjectQueuePriority
TKT-1042Refund for duplicate chargeBilling2
TKT-1043Export spins forever in the old browserBugs1
TKT-1044Invite link expired before the new hire used itAccess3

Editor

  • Name
    refund-check

    text

  • Input

    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 rowCount
number default required
DataGrid colCount
number default required
DataGrid readOnly
boolean default false
DataGrid focusableHeader
boolean default false
DataGrid activeCell
{row: number; col: number}
DataGrid onActiveCellChange
(cell: CellPosition) => void
DataGridTable aria-label
string default required
DataGridTable size
'sm' | 'default' | 'lg' default 'sm'
DataGridTable title, count, actions
ReactNode
DataGridRow index
number default required
DataGridHead col
number default required
DataGridCell row, col
number default required
DataGridCell label
string default required
DataGridCell value
string | number | boolean | null default null
DataGridCell 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 | null
DataGridCell cellId
string default 'row:col'
DataGridCell status
CellSaveState
DataGridCell mono
boolean default false

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

Install

For your coding agent

GitHub

Please star me pookie

141

A new design system every week

Leave your email before you copy the prompt.