Skip to content

Overview

The data grid is a React component for the screens where people spend the day with real data: sorting, faceted filters, fuzzy search, grouping with aggregation, selection with bulk actions, inline editing, spreadsheet-style keyboard ranges and clipboard, column pinning, reordering and resizing, virtualised rows, export, saved views, and a toolbar and pager already wired to all of it. It is built on TanStack Table v9 and the shadcn/ui primitives you already have, and it is installed as source into your project, so nothing sits between you and any of it.

Yuki Khanyuki.khan@mail.testChurnedTeamGrowthDirector$0.002174.6%Sep 2, 2023May 5, 2025, 10:52 PMnps-9Japan—
Mateo Rossimateo.rossi@corp.exampleInvitedFreePlatformPM$0.0016.2%Oct 21, 2024Jul 7, 2026, 9:33 PM—GermanyRenewal in Q1.
Aarav Sharmaaarav.sharma@example.comChurnedProSupportLead$0.00224.5%Feb 6, 2026May 4, 2026, 8:56 PM—France—
Vihaan Tanakavihaan.tanaka@corp.exampleChurnedProFinanceManager$0.00292.8%Oct 14, 2023Aug 27, 2024, 3:51 AM—IndiaRenewal in Q1.
Mia Menonmia.menon@example.comActiveFreeFinanceDirector$0.00127.8%May 19, 2024Jul 27, 2025, 2:01 PMnps-9Canada—
Mateo Nguyenmateo.nguyen@corp.exampleActiveEnterpriseDesignManager$2,449.003129.5%Feb 27, 2025Sep 15, 2026, 11:14 AMapacBrazilSpeaks at meetups.
Theo Silvatheo.silva@team.devActiveProResearchEngineer$36.00347.5%Feb 19, 2024May 29, 2024, 11:16 AM—Australia—
Theo Sharmatheo.sharma@example.comPausedTeamFinanceDirector$406.001432.6%Nov 9, 2023Feb 2, 2025, 6:06 PMapacCanada—
Diya Silvadiya.silva@corp.exampleActiveFreeDataDesigner$0.0014.7%Oct 29, 2025Jun 23, 2026, 7:48 AMchurn-riskeuCanadaAsked about invoices.
Saanvi Patelsaanvi.patel@corp.exampleActiveFreeDesignAnalyst$0.00143.4%Oct 23, 2026Oct 31, 2026, 5:04 AMvipBrazil—
0 of 300 row(s) selected.
Page 1 of 30

One page per feature, each with a live example and its code.

Feature What it does
Columns A typed builder with one call per kind; each kind brings its cell, sort, filter, editor, aggregation and format.
Sorting Click a header; shift-click to add a second key; custom sort functions.
Filtering Facet, text, number range, date range and boolean filters from the header, with chips in the toolbar.
Global search One box that fuzzy-matches across every searchable column.
Pagination Page size, page position, go-to, first/last, the selected count.
Row selection and bulk actions Checkboxes, shift-click ranges, select the page, a bar with your actions.
Row actions A menu per row, row click, row tinting.
Column visibility The View popover, hide from the header menu, start hidden.
Column pinning Pin columns to the start or end; they stay put while the rest scroll.
Column reordering Drag a header, or move left and right from its menu.
Column resizing Drag the edge; double-click to fit to content.
Expanding and tree data Nested rows from getSubRows, with an expander column.
Grouping and aggregation Group by any column; sums, means, counts and ranges in the group rows.
Virtualization Only the rows in view are in the DOM, automatically past 200 rows.
Inline editing Text, number, select, checkbox, date and textarea editors; validation; onCellEdit.
Cell selection and clipboard Focus a cell, drag or shift-arrow a range, copy it as TSV.
Keyboard navigation Arrows, Home/End, PageUp/PageDown, Enter, Space, Escape.
Export CSV, JSON and clipboard, formatted or raw, selected rows only.
Density Compact, comfortable or spacious rows.
Row pinning Keep a row at the top or bottom while you scroll and page.
Loading and empty states A progress bar, a spinner, an empty state and an error state with retry.
Saved views and persistence Remember the user’s view in storage; let them name and switch between views.
URL state Filters, search, sort and page in the address bar.
Server-side data The server pages, sorts and filters; the grid renders what it is given.
Accessibility A grid role, aria-sort, live announcements, focus that stays where you left it.
Internationalisation Every string in one labels object; numbers and dates through Intl.
Composition and custom layouts Place the parts yourself inside DataGridRoot.
API reference Every option, prop and type.

<DataGrid> renders these parts, top to bottom. Each is a component you can also render yourself, and each has a data-slot attribute you can style by.

  • DataGridRoot (data-slot="data-grid"): the provider and frame. Holds the grid api, the fullscreen state, the loading and error state, and the live region that announces changes.
    • DataGridToolbar (data-grid-toolbar): on the left, DataGridSearch, then DataGridFilterChips (one chip per active column filter) and DataGridGroupingBar (one chip per grouped column). On the right, your toolbarActions, then DataGridViewsMenu (with persist), DataGridGroupMenu, DataGridViewOptions, DataGridDensityMenu, DataGridExportMenu and DataGridFullscreenToggle.
    • DataGridBulkActions (data-grid-bulk-actions): appears while rows are selected, with the count, “Clear selection” and whatever you return from bulkActions.
    • DataGridTable (data-grid-viewport around data-grid-table): the scroll area and the table. Headers are DataGridColumnHeader (title, sort and filter indicators, the column menu, the filter popover, the resize handle). Cells render by column kind, or with your cell.
    • DataGridPagination (data-grid-pagination): the selected count or row count, rows per page, “Page x of y”, a go-to box past five pages, and first/previous/next/last.

<DataGrid> takes the options of useDataGrid plus the presentation props and renders every part above. This is the one to start with.

customers/page.tsx
import { DataGrid, createDataGridColumns } from "@/components/ui/data-grid"
const col = createDataGridColumns<Customer>()
const columns = col.columns([
col.select(),
col.text("name", { header: "Name" }),
col.currency("mrr", { header: "MRR", currency: "USD" }),
])
export function Customers({ customers }: { customers: Customer[] }) {
return (
<DataGrid
data={customers}
columns={columns}
getRowId={(c) => c.id}
height={560}
bulkActions={(rows) => <Button size="sm">Archive {rows.length}</Button>}
/>
)
}

useDataGrid builds the table and returns the api (grid.table is the TanStack table). Render the parts yourself inside DataGridRoot, in any order, with your own controls between them. Pass the same grid to <DataGrid grid={grid}> when you want the default layout but need the api outside the component too.

customers/page.tsx
import {
DataGridFilterChips,
DataGridPagination,
DataGridRoot,
DataGridSearch,
DataGridTable,
DataGridViewOptions,
useDataGrid,
} from "@/components/ui/data-grid"
export function Customers({ customers }: { customers: Customer[] }) {
const grid = useDataGrid<Customer>({ key: "customers", data: customers, columns, getRowId: (c) => c.id })
return (
<DataGridRoot grid={grid}>
<div className="flex items-center gap-2">
<h2 className="font-semibold">Customers</h2>
<DataGridSearch />
<DataGridFilterChips />
<DataGridViewOptions className="ml-auto" />
<Button size="sm">New customer</Button>
</div>
<DataGridTable height={480} />
<DataGridPagination />
</DataGridRoot>
)
}

Composition covers the parts in detail.

The grid turns on what a console page usually wants. Each default has an option to turn it off.

Default What you get Turn it off
Row selection col.select() renders checkboxes; shift-click selects a range; the header ticks the page. selection={false}, or selection={{ mode: "single" }} for one row at a time.
Cell selection Click a cell to focus it; drag or shift-arrow a range; ⌘C copies it. cellSelection={false}. Rows then toggle selection on click when there is no checkbox column.
Grouping “Group by” in each header menu, a Group menu in the toolbar, aggregated group rows. grouping={false}; groupable: false on one column.
Fuzzy search The toolbar search box matches by rank across every searchable column. globalFilter="includesString" for plain substring matching, or globalFilter={false} to remove the box.
Pagination, 10 rows A pager with 10, 20, 50 and 100 as the choices. pagination={false} shows every row; pagination={{ pageSize: 25, pageSizeOptions: [25, 50] }} changes the numbers.
Virtualisation past 200 rows Once the row model holds 200 rows or more, only the rows in view render. Needs a height or fillHeight. virtualize={false}; virtualize for always; virtualize={{ threshold: 500 }} to move the line.
Editing, when a column is editable Enter, F2 or double-click opens the editor for that kind. editing={false}.
Toolbar and pager Both render with every built-in control. toolbar={false}, toolbar={{ export: false }}, pager={false}.
Column hiding, pinning, resizing, reordering All on for every data column. Per column: enableHiding, enablePinning, enableResizing, draggable; grid-wide through table={{ enableColumnResizing: false }}.

Off by default: rowPinning, persist, server, striped, fillHeight.

TanStack Table v9 is the headless core (row models, state in a store, feature-typed APIs). shadcn/ui on Base UI supplies the menus, popovers, selects, checkboxes and buttons. TanStack Virtual renders only the rows in view, dnd kit moves the headers, and Tailwind CSS v4 styles it all through your theme tokens. The introduction has the table and the links.

  • API reference: every option of useDataGrid, every prop of <DataGrid> and the parts, every exported type.
  • Type safety: how the column builder, DataGridColumnDef<T> and TanStack v9’s feature typing keep a grid typed end to end.
  • Your first grid in 5 minutes if you have not built one yet.