Skip to content
Design System
Contact

Data grid

1,000 editable rows with sticky header and first column, windowed for speed.

ComponentsManual windowing

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
Select 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"
3
4// 1,000 deals from a seeded PRNG at module scope: identical on server and client.
5<DataGrid
6 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-0042
    Globex Labs
    $42,500
    Defaultrest
  • D-0042
    Globex Labs
    $42,500
    Hoverrow :hover
  • D-0042
    Globex Labs
    $42,500
    Focusaria-activedescendant
  • D-0042
    Globex Labs
    Loadingaria-busy
  • D-0042
    Globex Labs
    $42,500
    Disabledaria-readonly
  • D-0042
    Globex Labs
    -500

    Amount must be a positive number

    Erroraria-invalid
  • D-0042
    Globex Labs
    $42,500
    Successjust 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) + 1
4const start = Math.max(0, first - overscan)
5const end = Math.min(total, first + visible + overscan)
6
7// Spacers keep the scrollbar the height of all 1,000 rows.
8const padTop = start * rowHeight
9const padBottom = (total - end) * rowHeight
10
11// Re-render only when the first visible row changes.
12setScrollTop((prev) =>
13 Math.floor(prev / rowHeight) === first ? prev : scrollTop
14)

API

PropTypeDefaultDescription
rowsDeal[]dealsRow data. The default is 1,000 deals generated deterministically in grid-data.ts.
rowHeightnumber36Fixed row height in pixels. Windowing depends on it being constant.
overscannumber10Extra rows rendered above and below the viewport so fast scrolling never shows blank space.
heightnumber420Height of the scroll container, header included.
stickyFirstColumnbooleantruePins the row header column (checkbox and deal id) while scrolling sideways.
editablebooleantrueAllows editing cells whose column defines an editor: text, number, select or date.
selectablebooleantrueAdds row checkboxes, select all and the sum of selected amounts.
GridColumn.editor"text" | "number" | "select" | "date"—Editor used for the column. Columns without one are read-only.
parseCell(key, raw) => { value } | { error }—Validates an edit. The error text is shown on the cell and says how to fix it.

Accessibility

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" with aria-rowcount and aria-colcount for the full data set, and every rendered row carries its real aria-rowindex, so position is announced correctly even though only a window is in the DOM.
  • Focus stays on the grid and aria-activedescendant points at the active cell, so scrolling a cell out of the window never drops focus to the page.
  • Rows expose aria-selected inside an aria-multiselectable grid; the deal id cell is a rowheader.
  • Read-only cells set aria-readonly. An invalid cell sets aria-invalid and includes the fix as visually hidden text; the open editor links its error with aria-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.