Data grid
1,000 editable rows with sticky header and first column, windowed for speed.
ComponentsManual windowing
Preview theme
Playground
Click a cell, then use the arrow keys. Double-click or press Enter to edit; Enter or blur commits, Escape cancels. Try a negative amount to see validation. Only the rows in view are rendered.
Preview
Demo data
Rendering 22 of 1,000 rowsRows 1–220 selectedNo edits yet
↑↓←→ move Enter edit Esc cancel Space select rowSelect rows to total their amount
Props
Row height
Overscan
example.tsxtsx
1import { DataGrid } from "@/components/design-system/components/data-grid/data-grid-view"2import { deals } from "@/components/design-system/components/data-grid/grid-data"34// 1,000 deals from a seeded PRNG at module scope: identical on server and client.5<DataGrid6 rows={deals}7 height={420}8/>Cell states
The amount cell in each state. Invalid values stay visible and flagged so nothing is silently discarded; a committed edit flashes green for a moment.
States
- D-0042Globex Labs$42,500Default
rest - D-0042Globex Labs$42,500Hover
row :hover - D-0042Globex Labs$42,500Focus
aria-activedescendant - D-0042Globex LabsLoading
aria-busy - D-0042Globex Labs$42,500Disabled
aria-readonly - D-0042Globex Labs-500
Amount must be a positive number
Erroraria-invalid - D-0042Globex Labs$42,500Success
just committed
Windowing
No virtualisation library: with a fixed row height the visible range is a division, and two spacer divs stand in for the rows that are not rendered. Scrub the scroll position to see the numbers the grid uses.
Window inspector
Scroll position12,600px
Viewport Overscan1,000 rows
Overscan
- First visible row
- 351
- Rendered range
- 341–372
- Rows in the DOM
- 32 of 1,000
- Cells in the DOM
- 256 vs 8,000
- Top spacer
- 12,240px
- Bottom spacer
- 22,608px
use-data-grid.tsts
1// Fixed row height, so the visible range is arithmetic, not measurement.2const first = Math.floor(scrollTop / rowHeight)3const visible = Math.ceil(bodyHeight / rowHeight) + 14const start = Math.max(0, first - overscan)5const end = Math.min(total, first + visible + overscan)67// Spacers keep the scrollbar the height of all 1,000 rows.8const padTop = start * rowHeight9const padBottom = (total - end) * rowHeight1011// Re-render only when the first visible row changes.12setScrollTop((prev) =>13 Math.floor(prev / rowHeight) === first ? prev : scrollTop14)Keyboard
- Move the active cell
- ↑or↓or←or→
- Move one screen of rows
- Page UporPage Down
- First or last cell in the row
- HomeorEnd
- First row
- Ctrl+Home
- Edit the active cell
- EnterorF2
- Commit the edit (Tab moves right)
- EnterorTab
- Cancel the edit
- Esc
- Select or deselect the row
- Space
- Select or clear all rows
- ⌘+A
Semantics and ARIA
- The container is
role="grid"witharia-rowcountandaria-colcountfor the full data set, and every rendered row carries its realaria-rowindex, so position is announced correctly even though only a window is in the DOM. - Focus stays on the grid and
aria-activedescendantpoints at the active cell, so scrolling a cell out of the window never drops focus to the page. - Rows expose
aria-selectedinside anaria-multiselectablegrid; the deal id cell is arowheader. - Read-only cells set
aria-readonly. An invalid cell setsaria-invalidand includes the fix as visually hidden text; the open editor links its error witharia-describedby. - The last edit is announced through a polite live region, and the checkboxes stay out of the tab order because Space already selects the row.