Data gridPro
A spreadsheet grid with range selection, inline editing, a fill handle, and animated sorting.
Renewals
- Editing many values in place like a spreadsheet, such as budgets, price lists, or inventory.
- Data people move to and from Excel or Google Sheets with copy and paste.
- Work that needs range selection, a fill handle, fill down, and undo.
- Large flat tables, up to tens of thousands of rows, that people filter, sort, and total.
- Bulk work on checked rows: copy, CSV export, delete, or your own actions.
- Use sortable-data-table for read-only records with sorting and row selection.
- Use tree-table when rows nest under parents.
- Use a form when people edit one record at a time.
- Use a card list or card grid when each record needs rich content like images or long text.
Installation
Pro source and install commands unlock with a Pro plan.
import { useState } from "react";import { Archive } from "lucide-react";import { DataGrid, type DataGridColumn, type DataGridRow, type DataGridSort } from "@/registry/components/data-grid/data-grid"; const columns: DataGridColumn[] = [ { key: "item", label: "Item", width: 180, pinned: true }, { key: "status", label: "Status", type: "select", options: ["Planned", "Active", "Done"] }, { key: "q1", label: "Q1", type: "currency" }, { key: "q2", label: "Q2", type: "currency" }, { key: "margin", label: "Margin", type: "percent", decimals: 1, editable: false }, { key: "notes", label: "Notes", hidden: true, aggregate: "count" },]; export function Budget() { const [rows, setRows] = useState<DataGridRow[]>([ { id: "r1", item: "Design", status: "Active", q1: 12000, q2: 14500, margin: 32.5, notes: "" }, { id: "r2", item: "Hosting", status: "Planned", q1: 3200, q2: 3400, margin: 18, notes: "Annual plan" }, ]); const [sort, setSort] = useState<DataGridSort[]>([{ key: "q1", dir: "desc" }]); return ( <DataGrid label="Budget" columns={columns} rows={rows} onRowsChange={setRows} sort={sort} onSortChange={setSort} maxHeight={480} exportFileName="budget-2026" bulkActions={[{ label: "Archive", icon: <Archive size={16} aria-hidden />, onAction: checked => console.log(checked.map(r => r.id)) }]} /> );}API reference
5 parts. The first is the root.
DataGrid
A spreadsheet grid for editing tabular data in place. It has range selection, a fill handle, Excel and Sheets clipboard, undo, multi-column sort, a filter row, pinned and hidden columns, row checks with bulk actions, a totals row, and virtualized rows. Every piece of view state works controlled or uncontrolled.
columnsRequiredDataGridColumn[]–Column definitions in definition order. Pinned columns are drawn first.labelRequiredstring–Accessible name for the grid. Also names the exported CSV file.rowsDataGridRow[]–Controlled rows. Pair with onRowsChange.defaultRowsDataGridRow[]–Starting rows when uncontrolled.onRowsChange(rows: DataGridRow[]) => void–Called with the full row list after every edit, fill, paste, cut, clear, delete, undo, or redo.maxHeightnumber400Tallest the scrolling area grows before it scrolls, in px.sortDataGridSort[]–Controlled sort keys in priority order.defaultSortDataGridSort[][]Starting sort keys when uncontrolled.onSortChange(sort: DataGridSort[]) => void–Called when a header click, Shift-click, or header menu changes the sort.filtersDataGridFilters–Controlled column filters, keyed by column key.defaultFiltersDataGridFilters{}Starting filters when uncontrolled. The filter row starts open when any filter is set.onFiltersChange(filters: DataGridFilters) => void–Called when a filter changes or all filters are cleared.selectedRowIdsstring[]–Controlled checked row ids.defaultSelectedRowIdsstring[][]Starting checked row ids when uncontrolled.onSelectedRowIdsChange(ids: string[]) => void–Called with checked ids in data order.hiddenColumnsstring[]–Controlled hidden column keys.defaultHiddenColumnsstring[]–Starting hidden keys. Defaults to columns with hidden: true.onHiddenColumnsChange(keys: string[]) => void–Called when the columns menu or a header menu hides or shows a column.pinnedColumnsstring[]–Controlled pinned column keys.defaultPinnedColumnsstring[]–Starting pinned keys. Defaults to columns with pinned: true, or the first column when no column sets pinned.onPinnedColumnsChange(keys: string[]) => void–Called when a header menu pins or unpins a column.density"compact" | "standard" | "comfortable"–Controlled row density: 32, 40, or 48px rows.defaultDensity"compact" | "standard" | "comfortable""standard"Starting density when uncontrolled.onDensityChange(density: DataGridDensity) => void–Called when the density menu changes.rowSelectionbooleantrueShows the row gutter with row numbers and checkboxes.toolbarbooleantrueShows the toolbar with search, undo and redo, filters, columns, density, and export.totalsbooleantrueShows the sticky totals row.bulkActionsDataGridBulkAction[]–Extra actions for checked rows, shown between Export and Delete.canDeleteRowsbooleantrueShows Delete for checked rows. Deleting can be undone.exportFileNamestring–CSV file name without the extension. Defaults to a slug of label.loadingbooleanfalseShows six placeholder rows and sets aria-busy.emptyMessagestring"No rows yet"Shown when there are no rows at all. Filtered-out rows show "No rows match" with Clear filters.DataGridColumn
One column definition.
keyRequiredstring–Row field this column reads and writes.labelRequiredstring–Header text. Also used in CSV and copied headers.widthnumber140Starting width in px. The last column absorbs spare width on a wide grid.minWidthnumber72Narrowest a resize can reach. Resizing stops at 560px.type"text" | "number" | "currency" | "percent" | "select""text"Number, currency, and percent align right, parse typed numbers, and total in the footer. Select picks from options.decimalsnumber0Decimal places when formatting numeric values.editablebooleantruefalse makes the column read only. Edits, pastes, fills, and clears skip it.optionsstring[]–Choices for a select column. Defaults to the distinct values already in the column. Also sets the sort order.pinnedboolean–Starts pinned to the left edge. When no column sets this, the first column is pinned.hiddenboolean–Starts hidden. People can show it again from the columns menu.aggregate"sum" | "average" | "min" | "max" | "count" | "none"–What the totals row shows. Defaults to sum for number and currency, average for percent, none otherwise.sortablebooleantruefalse disables the header sort button and the sort menu items.filterablebooleantruefalse leaves the filter cell empty and drops Filter from the header menu.DataGridBulkAction
An extra toolbar action for checked rows.
labelRequiredstring–Button text. Also the React key, so keep it unique.iconReactNode–Icon before the label. The label hides below 640px of grid width.tone"danger"–Draws the button in the danger color.onActionRequired(rows: DataGridRow[]) => void–Receives the checked rows in data order.Types
Exported from data-grid.tsx.
DataGridValuestring | number | null–A cell value. Empty number cells store null.DataGridRow{ id: string; [key: string]: DataGridValue }–A row. id must be unique and stable.DataGridSort{ key: string; dir: "asc" | "desc" }–One sort key.DataGridFiltersRecord<string, string | string[]>–Text and number columns take a query string. Select columns take the list of values to keep.DataGridColumnType"text" | "number" | "currency" | "percent" | "select"–Column type.DataGridAggregate"sum" | "average" | "min" | "max" | "count" | "none"–Totals row aggregate.DataGridDensity"compact" | "standard" | "comfortable"–Row density.DataGridPropsinterface–Props of DataGrid.data-grid-model helpers
Pure functions exported from data-grid-model.ts, usable on the server or in tests.
format(value: DataGridValue | undefined, column: DataGridColumn) => string–Display text: en-US grouping, $ for currency, % for percent, a true minus sign for negatives.parse(text: string, column: DataGridColumn, options?: string[]) => { ok: true; value: DataGridValue } | { ok: false }–Reads typed or pasted text. Numbers accept $, %, commas, spaces, and accounting parentheses. Select values match options without case.numberFilter(expression: string) => ((value: number) => boolean) | null | "invalid"–Parses >100, >=100, <5, !=0, =12, 12, or a range like 10..50.rowMatcher(columns, filters, search, searchColumns?) => ((row: DataGridRow) => boolean) | null–One predicate for all filters and the quick search.sortRows(rows: DataGridRow[], sort: DataGridSort[], columns: DataGridColumn[]) => DataGridRow[]–Stable multi-key sort. Empty cells sink to the bottom either way.aggregate / defaultAggregate(rows, column, kind) => number | null / (column) => DataGridAggregate–The totals row math and its default per column type.toTsv / parseTsv / toHtml / parseHtmlTable(matrix: string[][]) => string / (text: string) => string[][] / ...–Clipboard formats that match Excel and Google Sheets, including quoted cells with tabs or line breaks.toCsv(columns: DataGridColumn[], rows: DataGridRow[]) => string–RFC 4180 CSV with a byte order mark so Excel reads UTF-8.extend(source: DataGridValue[], count: number, forward: boolean) => DataGridValue[]–Fill handle values. An evenly spaced number run continues its series; anything else repeats.- Arrow keys
- Move the active cell.
- Shift+arrows
- Extend the range from the active cell.
- Cmd/Ctrl+arrows
- Jump to the first or last row or column. Add Shift to extend the range.
- HomeorEnd
- Move to the first or last column in the row.
- Cmd/Ctrl+HomeorEnd
- Move to the first or last cell of the grid.
- PageUporPageDown
- Move by one visible page of rows.
- Cmd/Ctrl+A
- Select every cell.
- Ctrl+Space
- Extend the range to the whole of its columns.
- Shift+Space
- Check or uncheck the rows in the range. Needs rowSelection.
- EnterorF2
- Edit the active cell, keeping its value. Arrow keys then move the caret.
- Any character
- Start editing and replace the value. Arrow keys then commit and move, like Excel.
- Alt+ArrowDown
- On a select cell, open its option list.
- EnterorShift+Enter (editing)
- Commit and move down or up. An invalid value stays open and is marked.
- TaborShift+Tab (editing)
- Commit and move right or left.
- ArrowUporArrowDown (option list)
- Move through select options. Enter or Tab picks the highlighted one.
- Escape (editing)
- Close the option list, or cancel the edit.
- Escape
- Collapse the range to the active cell, or else clear checked rows.
- DeleteorBackspace
- Clear the range. Read-only cells are skipped.
- Cmd/Ctrl+D
- Fill down: copy the top row of the range into the rows below.
- Cmd/Ctrl+CorXorV
- Copy, cut, or paste. Works with Excel, Google Sheets, and Numbers.
- Cmd/Ctrl+Z
- Undo.
- Cmd/Ctrl+Shift+ZorCmd/Ctrl+Y
- Redo.
- Shift+click header
- Add the column as the next sort key. Plain click cycles ascending, descending, off.
- Alt+ArrowLeftorArrowRight (header)
- Narrow or widen the column by 16px.
- EnterorArrowDown (searchorfilter input)
- Return focus to the grid.
- Escape (searchorfilter input)
- Clear the input, or return to the grid when it is empty.
- The grid is one focusable role="grid" with aria-multiselectable, aria-rowcount, aria-colcount, and aria-busy while loading. The active cell is exposed through aria-activedescendant, not moving focus.
- Rows and cells carry aria-rowindex and aria-colindex, so counts stay right while rows are virtualized. The active and editing rows stay mounted when scrolled away.
- Cells report aria-selected and aria-readonly. Rows report aria-selected when checked.
- Headers are role="columnheader" with aria-sort. The sort button label names the direction, the sort priority, and the Alt + arrow resize keys.
- The cell editor is labelled by column and row name and sets aria-invalid on a bad value. On select columns it is a combobox that controls a listbox, with aria-activedescendant on the highlighted option.
- Row checkboxes are role="checkbox" and stay out of the tab order; the select-all box in the header is tabbable and shows a mixed state.
- Number filter inputs describe their syntax and set aria-invalid on an unreadable expression. Select filters open a labelled checklist.
- Edits, sorts, checks, copies, pastes, fills, undo, and redo are announced in a polite live region. The row count is also a polite live region.
- Totals cells carry a full aria-label, such as "Q1 sum $15,200".
- The selection rectangle and active cell spring between ranges on motionTokens.spring.snappy. Scrolling moves them at once, and resizing a column snaps them.
- After a sort, rows slide to their new place on the spring curve for 700ms, and rows arriving from off screen fade in.
- Pasted, filled, undone, and redone cells flash a wash that fades out over 0.9s.
- The toolbar swaps to bulk actions with a short slide, fade, and blur. Status messages enter the same way.
- Totals, the selection summary, and the checked count roll with animated-counter.
- An invalid edit shakes once. Menus and the option list fade and scale in from their anchor.
- Reduced motion jumps the selection, drops the row slide, the shake, the press scales, and the skeleton pulse, and keeps short fades.
- The grid scrolls in both directions inside maxHeight. The header and totals row stay sticky, and pinned columns and the row gutter stay on the left.
- Pinned columns are scaled down to about half of the grid width so a narrow grid still shows scrolling columns.
- Layout reacts to the grid's own width with container queries. Below 640px toolbar buttons become icons. Below 460px the row count and undo and redo buttons hide. Below 480px the status message wraps to its own line.
- On touch a pan scrolls the grid. A tap selects a cell on release, a second tap edits it, and only the fill handle takes over a touch drag.
- Row checkboxes and header menu buttons are always visible on devices without hover.
- Rows are virtualized: only the visible rows plus 6 above and below render, each positioned with translateY. Rows are memoized, so scrolling and typing re-render only the rows they touch.
- Filters and search use useDeferredValue, so typing a filter stays responsive on large data.
- The sorted and filtered view is recomputed only when sort, filters, search, visible columns, or the set of row ids change, not on every edit.
- Select column options are derived from up to 200 distinct values when options is not given; pass options for large or wide data.
- Columns are not virtualized, so keep column counts moderate. Undo holds up to 100 full row snapshots.
Notes for AI
Give your coding assistant the Markdown reference instead of screenshots.
- Choose it when people edit many values in place like a spreadsheet. Use sortable-data-table for read-only records and tree-table for nested rows.
- Every row needs a unique, stable id. Column keys map to row fields. Keep rows from onRowsChange to persist edits.
- Use number, currency, or percent for numeric data so parsing, right alignment, totals, and the status bar sum work. Store raw numbers, not formatted strings.
- Mark computed or locked columns editable: false. Give select columns options so pasted and typed values are checked against them.
- Copy writes tab separated text plus an HTML table with formatted values, so Excel and Sheets read typed numbers. Paste reads an HTML table first, then TSV with quoted cells. A selection that is a whole multiple of the clipboard is tiled; values that do not parse are skipped and reported.
- The bulk Copy button copies checked rows with a header row through navigator.clipboard, and Export downloads CSV of checked rows. The toolbar Export downloads all filtered rows in view order.
- A sort reorders once, like a spreadsheet. Editing a value does not re-sort or re-filter, so rows never jump under the cursor.
- Undo keeps 100 steps and covers edits, fills, pastes, clears, and row deletes. Sort, filter, and column changes are view state and are not undone.
- Text filters match accent-insensitive substrings; prefix = for an exact match or ! to exclude. The quick search matches every word across visible columns.
- All filtering and sorting run on the client over the rows you pass. For server data, control sort and filters and fetch on change.
The full library index for assistants is at /llms.txt.