Installation
npx gbs-add-block@latest -a DataGrid -betaThe block copies the data-grid folder into your project, along with the small shared folder that every component imports. You own the code and can change it freely.
There are no peer dependencies other than React.
Requirements
- React 19 and
@types/react19 - TypeScript target ES2022 or newer, with
"jsx": "react-jsx" - Browsers from 2024 or newer (the styles use
light-dark(),:has()and the Popover API)
Import the stylesheet once, for example in your global CSS:
@import "../component-lib/gbs.css";or in your root layout / entry file:
import "@/component-lib/gbs.css";The DataGrid is a virtualized data grid for React 19 and Next.js. It renders only the rows and columns on screen, so it stays fast with 100,000+ rows. It supports sorting, typed column filters, global search, pagination, row selection, column resizing, reordering, pinning and hiding, inline editing with validation, CSV / Excel / PDF export, keyboard navigation, dark mode and right-to-left layouts. It works with data held in the browser (client mode) or fetched page by page from an API (server mode).
Demo
Quick Start
"use client";
import { createColumnHelper, DataGrid } from "@/component-lib/data-grid";
interface Employee {
id: number;
name: string;
department: string;
salary: number;
startDate: string;
active: boolean;
}
const col = createColumnHelper<Employee>();
// Define columns outside the component, or wrap them in useMemo.
const columns = [
col.field("id", { header: "ID", type: "number", width: 80, pin: "left" }),
col.field("name", { width: 200 }),
col.field("department", {
options: [
{ label: "Engineering", value: "Engineering" },
{ label: "Sales", value: "Sales" },
],
}),
col.field("salary", {
type: "number",
format: (value) => `$${value.toLocaleString()}`,
}),
col.field("startDate", { header: "Start date", type: "date" }),
col.field("active", { type: "boolean" }),
];
export default function Employees({ data }: { data: Employee[] }) {
return (
<DataGrid
data={data}
columns={columns}
getRowId="id"
enableRowSelection
height={600}
onRowClick={(row) => console.log("Clicked", row)}
/>
);
}In this section
The Data Grid has more surface than one page can hold comfortably. This page covers installation, the props, and the things every grid needs; the rest is split by subject:
| Page | What is in it |
|---|---|
| Columns & Cells | Column options, custom cell templates, inline editing |
| Sorting, Filtering & Server Data | Sorting, filtering, search, selection, controlled state, server-side pagination |
| Asking in Words | The optional agent surface: ai, adapters, and WebMCP |
| Grid API & Export | The ref API, CSV/Excel/PDF export, utilities |
Props Table
Data
| Prop | Type | Default | Description |
|---|---|---|---|
data | T[] | required | The rows to display. In server mode, the rows of the current page. |
columns | ColumnDef<T>[] | required | Column definitions. See Column Options. |
getRowId | keyof T | (row: T, index: number) => string | row.id, else the array index | Stable row id used for selection, editing and React keys. Prefer a property name, e.g. getRowId="id". |
mode | "client" | "server" | "client" | client: the grid sorts, filters and paginates data. server: data is already the current page. |
rowCount | number | data.length | Total number of rows on the server. Used for pagination in server mode. |
loading | boolean | false | Shows a loading bar and sets aria-busy. Existing rows stay visible while loading. |
State
| Prop | Type | Default | Description |
|---|---|---|---|
initialState | Partial<GridState> | — | Starting state (sorting, filters, page size, column layout, …). The grid manages it afterwards. |
state | Partial<GridState> | — | Controlled state. Keys you pass here are owned by you: the grid reports changes through onStateChange but does not apply them. |
onStateChange | (next: GridState, prev: GridState) => void | — | Called on every state change (sorting, filters, selection, column layout, density, …). |
onQueryChange | (query: GridQuery) => void | — | Called when sorting, filters, search or pagination change. Use it to fetch data in server mode. |
Features
| Prop | Type | Default | Description |
|---|---|---|---|
enablePagination | boolean | true | Shows the pagination bar. When false, all rows scroll in one virtualized list. |
pageSizeOptions | number[] | [25, 50, 100, 250] | Choices in the "Rows per page" select. The default page size is 50. |
enableSorting | boolean | true | Header click, Enter key and the column menu sort columns. |
enableMultiSort | boolean | true | Shift + click (or Shift + Enter) adds a column to the sort. |
enableFiltering | boolean | true | Shows the filter form in the column menu. |
enableColumnResizing | boolean | true | Drag the header edge to resize. Escape while dragging abandons it; double-click the edge resets the width. |
enableColumnReordering | boolean | true | Drag headers to reorder, or use "Move earlier / later" in the column menu. |
enableColumnPinning | boolean | true | "Pin to start / end" in the column menu. |
enableColumnHiding | boolean | true | "Hide column" in the column menu and the Columns toolbar menu. |
enableRowSelection | boolean | (row: T) => boolean | false | Adds a checkbox column. Pass a function to allow selection only for some rows. |
selectionMode | "single" | "multiple" | "multiple" | single keeps at most one row selected and hides the select-all checkbox. |
toolbar | boolean | ToolbarOptions | true | Shows the toolbar. See Toolbar Options. |
Events
| Prop | Type | Default | Description |
|---|---|---|---|
onRowClick | (row: T, event: MouseEvent) => void | — | Row click. Not fired for clicks on buttons, links, inputs, selects, labels or elements with data-dg-interactive. |
onRowDoubleClick | (row: T, event: MouseEvent) => void | — | Row double-click, with the same exclusions. |
onCellEdit | (event: CellEditEvent<T>) => void | Promise<void> | — | Called when an edit is committed. Update your data here. Return a promise to show the pending value until it settles. |
Layout and Appearance
| Prop | Type | Default | Description |
|---|---|---|---|
height | number | string | 520 | Height of the scrolling area. Ignored when autoHeight is set. |
autoHeight | boolean | false | Grow to fit all rows instead of scrolling. Use with pagination or small data sets. |
rowHeight | number | from density: 32 / 40 / 52 | Fixed row height in pixels. |
headerHeight | number | max(40, rowHeight) | Header row height in pixels. |
emptyState | ReactNode | built-in message | Shown when there are no rows. |
getRowClassName | (row: T, rowIndex: number) => string | undefined | — | Extra class name per row. |
classNames | Partial<Record<GridSlot, string>> | — | Class names for root, toolbar, viewport, header, headerCell, row, cell, pagination. |
className | string | — | Class name for the root element. |
style | CSSProperties | — | Inline style for the root element (e.g. CSS variables). |
locale | string | browser locale | BCP 47 locale for dates and numbers, e.g. "en-IN". |
localeText | Partial<LocaleText> | English | Overrides UI text. See Locale Text. |
aria-label | string | "Data grid" | Accessible name of the grid. |
exportFileName | string | "export" | Default file name for exports. |
ref | Ref<GridApi<T>> | — | Imperative API. See Grid API. |
Toolbar Options
Pass an object to toolbar to choose what the toolbar shows. Omitted options default to true.
| Option | Type | Default | Description |
|---|---|---|---|
search | boolean | true | Global search box. |
filterChips | boolean | true | Chips for active filters, each with a remove button, plus "Clear all". |
columns | boolean | true | "Columns" menu to show or hide columns and reset the layout. |
density | boolean | true | "Density" menu: compact, standard, comfortable. |
export | boolean | true | "Export" menu: CSV, Excel, PDF. |
start | ReactNode | — | Custom content at the start of the toolbar. |
end | ReactNode | — | Custom content at the end of the toolbar. |
<DataGrid
data={data}
columns={columns}
toolbar={{
density: false,
end: <button onClick={openCreateDialog}>Add employee</button>,
}}
/>Keyboard Navigation
The grid follows the WAI-ARIA grid pattern. One cell is focusable at a time; Tab moves focus into and out of the grid.
| Keys | Action |
|---|---|
| Arrow keys | Move between cells, including the header row. |
| Home / End | First / last cell in the row. |
| Ctrl + Home / Ctrl + End | First header cell / last cell of the last row. |
| Page Up / Page Down | Move up / down by a screenful of rows. |
| Enter (header) | Sort the column. Shift + Enter adds it to the sort. |
| Alt + ↓ or Menu key (header) | Open the column menu. |
| Enter / F2 (cell) | Start editing, or move focus into a control inside the cell. |
| Space | Toggle row selection (Shift + Space selects a range). On the checkbox header, toggles all. |
| Ctrl / Cmd + A | Select all rows. |
| Ctrl / Cmd + C | Copy selected rows, or the focused cell. |
| Escape | Cancel editing, return focus from a control to its cell, or abandon a column resize in progress. |
Styling and Theming
The grid ships with default styles in styles.css. Rules ship unlayered, so a framework reset cannot strip them and a utility class passed through className still overrides them. Import the generated gbs.css once — see Theming.
<DataGrid
data={data}
columns={columns}
className="rounded-xl shadow-sm"
classNames={{
headerCell: "uppercase tracking-wide",
row: "hover:bg-indigo-50",
}}
getRowClassName={(row) => (row.active ? undefined : "opacity-60")}
/>CSS variables
Override them on .dg-root, any ancestor, or through style.
Every --dg-* below first looks for the shared --gbs-* variable of the same name, so one
palette on :root themes the grid and every other component together:
:root {
--gbs-accent: #7c3aed;
--gbs-radius: 12px;
--gbs-font-size: 14px;
}Set --dg-* when you want to change the grid alone, or to override a shared value for it:
.dg-root {
--dg-accent: #7c3aed;
--dg-radius: 12px;
--dg-font-size: 14px;
}| Variable | Used for |
|---|---|
--dg-font-size | Base font size (default 13px). |
--dg-bg, --dg-fg | Background and text color. |
--dg-muted | Secondary text. |
--dg-border, --dg-border-subtle | Outer / header borders and row separators. |
--dg-header-bg, --dg-header-fg | Header row. |
--dg-row-alt | Alternate row background. |
--dg-row-hover | Row hover background. |
--dg-row-selected, --dg-row-selected-hover | Selected rows. |
--dg-hover | Hover background of buttons and menu items. |
--dg-input-bg | Inputs, search box and editors. |
--dg-accent, --dg-accent-fg | Primary color and text on it. |
--dg-accent-soft, --dg-accent-strong | Filter chips. |
--dg-focus | Focus and editing rings. |
--dg-danger | Validation errors. |
--dg-pin-shadow | Shadow at the edge of pinned columns. |
--dg-shadow | Popover shadow. |
--dg-radius | Corner radius. |
--dg-cell-px | Horizontal cell padding (default 12px). |
Dark mode
Colors follow the page's color-scheme. To force a scheme, put class="dark" or data-theme="dark" (or "light") on an ancestor such as <html>.
Data attributes
Use these to style states, for example .dg-row[data-selected] or Tailwind's data-[active]:.
| Element | Attributes |
|---|---|
Root (.dg-root) | data-density, data-stale (while filtering large data), data-resizing |
Row (.dg-row) | data-selected, data-odd, data-row-index, aria-selected |
Cell (.dg-cell) | data-active, data-editable, data-editing, data-pending, data-invalid, data-pinned, data-pin-edge, data-align, data-col-index |
Header cell (.dg-header-cell) | aria-sort, data-sortable, data-active, data-pinned, data-pin-edge, data-align, data-drop (while dragging) |
Locale Text
Override any text with localeText. Dates and numbers use the locale prop.
<DataGrid
locale="de-DE"
localeText={{
searchPlaceholder: "Suchen…",
noRows: "Keine Daten",
selected: (count) => `${count} ausgewählt`,
pageRange: (from, to, total) => `${from}–${to} von ${total}`,
}}
/>| Key | Default |
|---|---|
gridLabel | "Data grid" |
search | "Search" |
searchPlaceholder | "Search…" |
columns | "Columns" |
resetColumns | "Reset columns" |
export | "Export" |
exportCsv | "CSV" |
exportExcel | "Excel (.xlsx)" |
exportPdf | "PDF (.pdf)" |
print | "Print…" |
density | "Density" |
densityCompact | "Compact" |
densityStandard | "Standard" |
densityComfortable | "Comfortable" |
selected | (count) => "{count} selected" |
clearSelection | "Clear" |
clearFilters | "Clear all" |
removeFilter | (label) => "Remove filter: {label}" |
filtered | "Filtered" |
noRows | "No rows" |
noResults | "No rows match the current filters" |
loading | "Loading…" |
pagination | "Pagination" |
rowsPerPage | "Rows per page" |
pageRange | (from, to, total) => "{from}–{to} of {total}" |
page | "Page" |
pageOf | (total) => "of {total}" |
firstPage | "First page" |
previousPage | "Previous page" |
nextPage | "Next page" |
lastPage | "Last page" |
columnMenu | (header) => "{header} column options" |
sortAscending | "Sort ascending" |
sortDescending | "Sort descending" |
clearSort | "Clear sort" |
pinLeft | "Pin to start" |
pinRight | "Pin to end" |
unpin | "Unpin" |
hideColumn | "Hide column" |
moveLeft | "Move earlier" |
moveRight | "Move later" |
filter | "Filter" |
filterValue | "Value" |
filterFrom | "From" |
filterTo | "To" |
filterOperator | "Condition" |
applyFilter | "Apply" |
clearFilter | "Clear" |
operators | Labels for every filter operator, e.g. contains: "contains", in: "is any of" (pass all 16 when overriding) |
yes / no | "Yes" / "No" |
selectRow | "Select row" |
selectAllRows | "Select all rows" |
Next.js
- Every component file starts with
"use client", soDataGridworks in the App Router. - Column definitions contain functions, which can't be passed from a Server Component to a Client Component. Define columns in a client module (a file with
"use client") and render the grid there. - A Server Component can fetch the first page and pass
data,rowCountandinitialState(plain values) to that client component.
Migrating from the Previous Grid
| Previous | New |
|---|---|
dataSource (array) | data |
dataSource (URL string) | Fetch in your component and pass data. The grid no longer fetches. |
lazy + pageSettings.totalCount | mode="server" + rowCount |
pageSettings.pageNumber / pageSize | initialState={{ pagination: { pageIndex: 0, pageSize: 10 } }} |
enableSearch, enableExcelExport, enablePdfExport | toolbar={{ search, export }} (both on by default) |
excelName, pdfName | exportFileName, or fileName in exportExcel() / exportPdf() |
pdfOptions | exportPdf({ orientation, paperSize, margin, fontSize, header, footer, theme }) |
selectAll, onSelectRow | enableRowSelection, onStateChange / getSelectedRows() |
isFetching | loading |
rowChange (row click) | onRowClick |
rowChange (from templates) | Pass your own callbacks to the component in cell |
pageStatus, activeFilterArrayValue, searchParamValue, onSearch | onQueryChange |
initialFilters, initialSearchParam | initialState={{ filters, globalFilter }} |
showTotalPages | Always shown ("1–25 of 1,000") |
onToolbarButtonClick | toolbar={{ export: false, end: <YourButtons /> }} and the Grid API |
gridContainerClass, tableHeaderStyle, gridColumnStyle, other class props | className, classNames, getRowClassName, CSS variables |
Column headerText | header |
Column template | cell |
Column filter: true | Filtering is on by default; use filterable: false to turn it off |
Column tooltip | Not built in. Use cell, e.g. cell: ({ value }) => <span title={value}>{value}</span> |
Column showInPdf, showInExcel | exportable, exportValue |
Filter { filterColumn, filterCondition, filterValue } | { columnId, operator, value } |
ref.goToPage(n), nextPage(), handleSearch(), getActiveFilters() | setPageIndex(n), setGlobalFilter(), getState().filters |
usePaginatedData hook | Not needed. See Server Side Pagination. |
Notes
- Rows have a fixed height (set by density or
rowHeight). Variable-height rows are not supported. - Grouping, tree data and pivoting are not supported yet.
- Client mode handles 100,000+ rows. For millions of rows, use server mode.