# shadcn-react-table > A shadcn/ui data table with Material React Table parity — sorting, filtering, grouping, editing, virtualization, pinning, keyboard navigation and more — built on TanStack Table v9. It is a shadcn registry block: the source is copied into your project and uses your own shadcn components and theme. Install into a project set up with shadcn/ui and Tailwind CSS v4: ```bash npx shadcn@latest add https://monabbir-ahmmad.github.io/shadcn-react-table/r/data-table.json ``` Then `import { DataTable, useDataTable } from "@/components/ui/data-table"`: build the table with `const table = useDataTable({ data, columns, getRowId })` and render ``. # Shadcn React Table A **shadcn/ui data table** with [Material React Table](https://www.material-react-table.com/) (MRT V3) feature parity, built on [TanStack Table v9](https://tanstack.com/table/latest). It ships as a shadcn **registry** component — you install the source into your own project with the shadcn CLI, so it inherits your style, base color, and theme tokens and you own every line. Example (`advanced`): ```tsx function PlaygroundExample() { const [data, setData] = React.useState(() => baseUsers) const columns = useUserColumns() // Every switch in the Settings menu, starting from the kitchen sink. const defaults = { rowSelection: true, rowNumbers: true, rowPinning: true, rowOrdering: false, detailPanel: false, columnOrdering: true, columnPinning: true, columnResizing: true, grouping: true, columnFilters: true, stickyHeader: true, stickyFooter: true, animations: true, virtualization: false, editing: true, } type Feature = keyof typeof defaults const [features, setFeatures] = React.useState(defaults) const [editMode, setEditMode] = React.useState("row") // One row per feature: the row is the toggle, the switch shows its state. const toggle = (feature: Feature, label: string) => ( { // Keep the menu open while switching several features. event.preventDefault() setFeatures((prev) => ({ ...prev, [feature]: !prev[feature] })) }} > {label} ) const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableMultiSort: true, enableRowSelection: features.rowSelection, enableRowNumbers: features.rowNumbers, enableRowPinning: features.rowPinning, enableRowOrdering: features.rowOrdering, onRowOrderChange: (activeId, overId) => setData((prev) => { const from = prev.findIndex((r) => r.id === activeId) const to = prev.findIndex((r) => r.id === overId) const next = [...prev] next.splice(to, 0, ...next.splice(from, 1)) return next }), renderDetailPanel: features.detailPanel ? ({ row }) => (

{row.original.email} · {row.original.city}, {row.original.country}

) : undefined, enableColumnOrdering: features.columnOrdering, enableColumnPinning: features.columnPinning, enableColumnResizing: features.columnResizing, enableGrouping: features.grouping, enableColumnFilters: features.columnFilters, enableStickyHeader: features.stickyHeader, enableStickyFooter: features.stickyFooter, enableAnimations: features.animations, // Virtualization scrolls through every row instead of paging them. enableRowVirtualization: features.virtualization, enablePagination: !features.virtualization, enableEditing: features.editing, editDisplayMode: editMode, initialState: { showColumnFilters: true, pagination: { pageSize: 10 } }, onEditingRowSave: ({ row, values, exit }) => { setData((prev) => prev.map((r) => r.id === row.id ? { ...r, ...(values as Partial) } : r ) ) exit() }, onEditingCellSave: ({ row, column, value }) => setData((prev) => prev.map((r) => (r.id === row.id ? { ...r, [column.id]: value } : r)) ), renderRowActions: ({ row }) => ( ), renderTopToolbarCustomActions: () => ( Rows {toggle("rowSelection", "Selection")} {toggle("rowNumbers", "Row numbers")} {toggle("rowPinning", "Row pinning")} {toggle("rowOrdering", "Row dragging")} {toggle("detailPanel", "Detail panel")} Columns {toggle("columnOrdering", "Ordering")} {toggle("columnPinning", "Pinning")} {toggle("columnResizing", "Resizing")} {toggle("grouping", "Grouping")} {toggle("columnFilters", "Column filters")} Display {toggle("stickyHeader", "Sticky header")} {toggle("stickyFooter", "Sticky footer")} {toggle("animations", "Animations")} {toggle("virtualization", "Virtualization")} Editing {toggle("editing", "Editing")} Mode: {editMode} setEditMode(mode as EditDisplayMode)} > {(["row", "cell", "modal", "table"] as const).map((mode) => ( {mode} ))} { setFeatures(defaults) setEditMode("row") }} > Reset ), }) return } ``` ## How it works It is a thin, opinionated layer over TanStack Table: - It wraps TanStack's `useTable` with sensible defaults (row models wired for you, an auto-injected selection column, localization) and adds to the instance: its UI state in `table.getState()`, methods such as `table.setDensity`, and its resolved options on `table.dataTable`. - Controlled state (`sorting`, `columnFilters`, `pagination`, `rowSelection`, …), the `manual*` server-side flags, and `getRowId` all **pass straight through** to TanStack. If you know TanStack Table, you already know this table. - The `` component reads `table.dataTable` to render the toolbar, filter row, body, footer, and pagination — no prop drilling. ## For AI tools Coding agents can install and use the table from one prompt, a skill file, the shadcn MCP server, or the docs as Markdown (`/llms.txt`, and `.md` after any page URL). See [AI agent setup](/docs/ai). ## Start here - [Installation](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/installation): The data table is distributed as a shadcn registry item. - [Quick start](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/quick-start): A minimal table is two pieces: columns (TanStack column defs, typed with DataTableColumnDef) and a call to useDataTable, whose result you hand to . - [AI agent setup](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/ai): Set up the data table with a coding agent — Claude Code, Cursor, Codex, Copilot and the like. - [Migrating from MRT](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/migrating-from-mrt): The data table follows Material React Table (MRT) closely: the same hook-then-component shape, most of the same options, and the same TanStack Table underneath. ## Popular guides - [Column filtering](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/column-filtering): Each column can expose its own filter control in a per-column filter row. - [Editing](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/editing): Set enableEditing: true to make rows editable, then choose how editing is presented with editDisplayMode. - [Server-side data](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/server-side): When your data lives on a server, hand pagination, sorting, and filtering off to it. - [Part props (slotProps)](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/slot-props): Every part of the table — the scroll surface, rows, cells, toolbars, the search box, edit fields and more — accepts extra props through the slotProps option. - [Virtualization](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/virtualization): Virtualization renders only the cells currently in view, keeping large tables fast. - [Row selection](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/row-selection): Set enableRowSelection: true to add a leading checkbox column. [Browse all guides →](/docs/guides) --- # Installation The data table is distributed as a **shadcn registry** item. The CLI installs the table source into your project, pulls the shadcn primitives it depends on (in *your* configured style), and installs the npm packages it needs. ## Prerequisites - A project already set up with **shadcn/ui** (a `components.json` with your style, base color, and `@/components/ui` aliases). If you don't have one, run `pnpm dlx shadcn@latest init` first. - **Tailwind CSS v4** and React 19 / Next.js (or any bundler that compiles your `components/`). > **Any shadcn style works — Radix UI or Base UI:** It only uses standard primitive APIs, so it adapts to any shadcn style (Sera, Vega, Nova, Luma, …), base color, **and component library flavor**. The same URL works in Radix-flavored and Base-UI-flavored projects: a current shadcn CLI (4.x — the one with `init -b radix|base`) adapts the source to your flavor at install time and pulls the primitives in *your* style. Nothing is overwritten unless you opt in. ## Add the component Run the shadcn CLI against the registry URL: ```bash pnpm dlx shadcn@latest add https://monabbir-ahmmad.github.io/shadcn-react-table/r/data-table.json # or: npx shadcn@latest add https://monabbir-ahmmad.github.io/shadcn-react-table/r/data-table.json · yarn dlx shadcn@latest add https://monabbir-ahmmad.github.io/shadcn-react-table/r/data-table.json · bunx --bun shadcn@latest add https://monabbir-ahmmad.github.io/shadcn-react-table/r/data-table.json ``` This single command: 1. **Installs npm dependencies** — `@tanstack/react-table@^9`, `@tanstack/react-virtual`, `@tanstack/match-sorter-utils@^9`, `@dnd-kit/*`, `date-fns`, `lucide-react`. The headless component library (Radix UI or Base UI) comes with the primitives below, matching your flavor. 2. **Pulls the shadcn primitives** it uses (`badge`, `button`, `calendar`, `checkbox`, `command`, `context-menu`, `dialog`, `dropdown-menu`, `input`, `label`, `popover`, `select`, `skeleton`, `slider`, `table`, `tooltip`) in your style — and skips any you already have. 3. **Writes the table** to `components/ui/data-table/`. 4. **Injects design tokens** (`--highlight`, `--highlight-foreground`) into your `globals.css`. ## Use a namespace (optional) To install by name instead of URL, add the registry to your `components.json`: ```json { "registries": { "@shadcn-react-table": "https://monabbir-ahmmad.github.io/shadcn-react-table/r/{name}.json" } } ``` Then: ```bash pnpm dlx shadcn@latest add @shadcn-react-table/data-table # a language, e.g. German pnpm dlx shadcn@latest add @shadcn-react-table/data-table-locale-de ``` ## Verify Import the public API from your `@/components/ui/data-table` alias: ```tsx import { DataTable, useDataTable } from "@/components/ui/data-table" ``` > **react-day-picker v9+:** If your shadcn `calendar.tsx` predates react-day-picker v9, its `classNames` may use the old `table` key (renamed to `month_grid`). Re-run `shadcn add calendar` or rename the key — this only affects the date filter variant. The calendar itself comes from your own shadcn setup via `registryDependencies` — the table isn't tied to react-day-picker, and any calendar with a compatible props surface drops in. ## Next Head to the [Quick start](/docs/quick-start) to render your first table. --- # AI agent setup Set up the data table with a coding agent — Claude Code, Cursor, Codex, Copilot and the like. ## Ask your agent Paste this into your agent: ```text Install shadcn-react-table in my app: https://monabbir-ahmmad.github.io/shadcn-react-table/SKILL.md ``` The prompt points the agent at the skill file: how to install the table (running `shadcn init` first when the project has no `components.json`), the rules that keep tables correct, where the option references are, and how to pull a docs example into the project. Every example on this site also has a **Copy prompt** button that asks your agent to add that example. ## Install the skill The same file installs as a skill, so your agent knows the table before you ask: ```bash npx skills add Monabbir-Ahmmad/shadcn-react-table ``` ## Registry address ```bash pnpm dlx shadcn@latest add https://monabbir-ahmmad.github.io/shadcn-react-table/r/data-table.json # or: npx shadcn@latest add https://monabbir-ahmmad.github.io/shadcn-react-table/r/data-table.json · yarn dlx shadcn@latest add https://monabbir-ahmmad.github.io/shadcn-react-table/r/data-table.json · bunx --bun shadcn@latest add https://monabbir-ahmmad.github.io/shadcn-react-table/r/data-table.json ``` The same address works in Radix UI and Base UI projects; the shadcn CLI adapts the source. Languages are separate items: `…/r/data-table-locale-de.json`. To install by name, register the namespace once in `components.json` (see [Installation](/docs/installation#use-a-namespace-optional)): ```json { "registries": { "@shadcn-react-table": "https://monabbir-ahmmad.github.io/shadcn-react-table/r/{name}.json" } } ``` ## MCP The shadcn MCP server lets an agent browse and install from the registries in your `components.json`. Register the namespace above, then set up your client: ```bash pnpm dlx shadcn@latest mcp init --client claude # or cursor, vscode, codex, opencode ``` The agent can then find and add `@shadcn-react-table/data-table` and its languages. ## Docs as Markdown - [`/llms.txt`](https://monabbir-ahmmad.github.io/shadcn-react-table/llms.txt) — an index of every page. - [`/llms-full.txt`](https://monabbir-ahmmad.github.io/shadcn-react-table/llms-full.txt) — every page in one file. - Any page: add `.md` to its URL, for example [`/docs/guides/editing.md`](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/editing.md). **Copy page** at the top of each page copies the same text. ## Project rules For `CLAUDE.md` or `AGENTS.md` when the skill isn't installed: ```markdown ## Data table - components/ui/data-table is shadcn-react-table (installed from its shadcn registry; edit freely). - Before changing table options read https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table.md; column options: https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/column-options.md. - Pass `getRowId`, and keep `data` and `columns` stable (useMemo, useState or module scope). - Table-specific column options go in `meta`; state goes through `initialState` / `state` + `on…Change`. - Reinstall: pnpm dlx shadcn@latest add https://monabbir-ahmmad.github.io/shadcn-react-table/r/data-table.json --overwrite (replaces local edits in components/ui/data-table). ``` ## Related - [Installation](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/installation): The data table is distributed as a shadcn registry item. - [Migrating from Material React Table](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/migrating-from-mrt): The data table follows Material React Table (MRT) closely: the same hook-then-component shape, most of the same options, and the same TanStack Table underneath. --- # Quick start A minimal table is two pieces: **columns** (TanStack column defs, typed with `DataTableColumnDef`) and a call to **`useDataTable`**, whose result you hand to **``**. 1. **Define columns** Columns are TanStack column defs, typed with `DataTableColumnDef`. Give rows a stable `id` so selection and state survive re-renders. ```tsx "use client" import * as React from "react" import { DataTable, useDataTable, type DataTableColumnDef, } from "@/components/ui/data-table" type Person = { id: string; name: string; email: string; age: number } const data: Person[] = [ { id: "1", name: "Ada Lovelace", email: "ada@example.com", age: 36 }, { id: "2", name: "Alan Turing", email: "alan@example.com", age: 41 }, { id: "3", name: "Grace Hopper", email: "grace@example.com", age: 85 }, ] const columns: DataTableColumnDef[] = [ { accessorKey: "name", header: "Name" }, { accessorKey: "email", header: "Email" }, { accessorKey: "age", header: "Age", meta: { align: "right" } }, ] ``` 2. **Create the table** Inside your component, pass `data` and `columns` to `useDataTable`. Everything else is optional; `getRowId` gives rows a stable identity and `initialState` seeds the page size. 3. **Render it** Hand the instance to ``. ```tsx export function PeopleTable() { const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { pagination: { pageSize: 10 } }, }) return } ``` That already gives you sorting, client pagination, a toolbar with global search, column visibility, and density controls: Example (`basic`): ```tsx function BasicExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { pagination: { pageSize: 10 } }, }) return } ``` ## Turning features on Every feature is an option on `useDataTable`. Flip a flag to enable it — for example, multi-column sorting, per-column filters, and row selection: ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableMultiSort: true, initialState: { showColumnFilters: true }, enableRowSelection: true, }) ``` > **Memoize data and columns:** Pass stable references for `data` and `columns` (e.g. via `React.useMemo` or > module scope). Handing TanStack a new `columns` identity every render can > reset state. Example (`minimal`): ```tsx function MinimalExample() { const data = React.useMemo(() => baseUsers.slice(0, 8), []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, // Turn off most of the built-ins for a plain, read-only table. enableTopToolbar: false, enablePagination: false, enableSorting: false, enableColumnFilters: false, enableColumnActions: false, enableKeyboardNavigation: false, }) return } ``` ## Where to go next - [Usage](/docs/usage) — the options-in / instance-out model and `table.dataTable`. - [Guides](/docs/guides/sorting) — enable each feature, with live examples. - [API reference](/docs/api/use-data-table) — every option and its default. --- # Usage The data table has one core idea: **options in, instance out.** ```tsx const table = useDataTable(options) // enriched TanStack instance return // renders it ``` ## The hook `useDataTable(options)` wraps TanStack's `useTable`. It: - supplies default row models (core, sorted, filtered, faceted, grouped, expanded, paginated) — TanStack v9 no longer accepts row-model options, so the table wires them itself; - injects display columns when needed (selection, row numbers, drag handle, expand, row actions); - merges localization and icons (built-in defaults → [`DataTableConfigProvider`](/docs/guides/custom-icons) → per-call options); - adds to the instance: UI state in **`table.getState()`**, methods such as **`table.setDensity`**, and the resolved options on **`table.dataTable`**. Because it extends TanStack's `TableOptions`, **anything TanStack accepts passes through**: controlled `state`, `on*Change` handlers, `manual*` server-side flags, `getRowId`, `getSubRows`, `initialState`, and so on. ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, // data-table options enableGrouping: true, // standard TanStack options, passed straight through state: { sorting }, onSortingChange: setSorting, manualPagination: true, rowCount: total, }) ``` ## The component `` reads `table.dataTable` and renders the full layout: top toolbar → alert banner → drop-to-group zone → bordered scroll surface (sticky header, optional filter row, body, optional sticky footer) → pagination. Any extra `div` props land on its root — see [DataTable props](/docs/api/data-table-props). To style or add props to an inner part, such as bounding the scroll surface for sticky headers or virtualization, use [`slotProps`](/docs/guides/slot-props): ```tsx const table = useDataTable({ data, columns, slotProps: { surface: { className: "max-h-[600px]" } }, }) ``` ## The instance The table's parts read their configuration from the instance rather than via props. You can use it too — for toolbar actions, custom layouts, or programmatic control: ```tsx renderTopToolbarCustomActions: ({ table }) => ( ) ``` The UI state (density, full screen, filter visibility, editing…) is in `table.getState()`, its methods are on `table` (`setDensity`, `beginRowEdit`, `cancelEdit`, …), and every resolved option and DOM ref is on `table.dataTable`. See the full [Table instance](/docs/api/table-instance) reference. ## Columns Columns are TanStack column defs, typed with `DataTableColumnDef`. The table reads extra per-column configuration from **`columnDef.meta`** — filter variant, alignment, edit variant, aggregation labels, click-to-copy, and more. See [Column options](/docs/api/column-options). ```tsx const columns: DataTableColumnDef[] = [ { accessorKey: "status", header: "Status", meta: { filterVariant: "multi-select", filterSelectOptions: STATUS_OPTIONS, }, }, { accessorKey: "salary", header: "Salary", aggregationFn: "sum", meta: { align: "right", filterVariant: "range-slider", editVariant: "number", }, }, ] ``` ### Column helper `createDataTableColumnHelper` is TanStack's column helper with the table's features filled in. `accessor` infers each column's value type from a key or an accessor function, so `getValue()` in `cell` is typed without annotations, and `meta` takes the table's column options. ```tsx import { createDataTableColumnHelper } from "@/components/ui/data-table" const helper = createDataTableColumnHelper() const columns = helper.columns([ helper.accessor("name", { header: "Name" }), helper.accessor((row) => row.salary / 12, { id: "monthly", header: "Monthly", cell: ({ getValue }) => getValue().toFixed(0), // getValue(): number meta: { align: "right" }, }), helper.display({ id: "actions", cell: ({ row }) => }), ]) ``` ## App-wide defaults Wrap a subtree in `DataTableConfigProvider` to set default `icons` and `localization` for every table inside it, without editing each call site: ```tsx import { DataTableConfigProvider } from "@/components/ui/data-table" ; ``` Precedence is: built-in defaults → provider → per-call `useDataTable({ localization, icons })`. --- # Updating your copy `shadcn add` copies this block's source into your project — after that, the code is yours, and upstream releases don't reach it automatically. This page is the manual update path. ## Know what you have Every release stamps its version into the registry item. When you install, note the `meta.version` of [`/r/data-table.json`](/r/data-table.json) somewhere in your project (a comment, your PR description). The [changelog](https://github.com/Monabbir-Ahmmad/shadcn-react-table/blob/main/packages/shadcn-react-table/CHANGELOG.md) lists what changed in each version. ## Update recipe 1. Start from a clean branch. 2. Re-run the install command with `--overwrite`: ```bash npx shadcn@latest add --overwrite /r/data-table.json ``` 3. Review the diff. Everything under `components/ui/data-table/` is replaced with the new upstream source — `git diff` shows upstream changes mixed with the loss of any local edits you made. 4. Re-apply your local edits on top, or cherry-pick hunks with `git checkout -p` against the pre-update commit. 5. Check the changelog for entries marked as breaking and adjust your usage. > **Keep local edits separate:** The less you edit the copied files directly — prefer wrapping the table or > using the many `render*` slots — the closer step 3's diff is to a pure > upstream update. ## Localization keys and installed locales When a release adds localization keys, re-add every locale you installed, otherwise its typecheck fails on the missing keys: ```bash pnpm dlx shadcn@latest add --overwrite /r/data-table-locale-de.json ``` Do this after re-adding the table, and re-apply any edits you made to a locale file. ## Upgrading to 1.0.0 1.0.0 moves the engine to TanStack Table v9 and settles the API. Re-add the block, then: 1. **Type columns with `DataTableColumnDef`.** TanStack v9's `ColumnDef` needs a feature-set type parameter; ours supplies it. ```ts import type { DataTableColumnDef } from "@/components/ui/data-table" const columns: DataTableColumnDef[] = [...] ``` `DataTableRow`, `DataTableColumn`, `DataTableCell`, and `DataTableHeader` are the matching instance types. Install `@tanstack/react-table@^9` and `@tanstack/match-sorter-utils@^9` (the registry item pins both). 2. **Drop row-model options.** `getCoreRowModel`, `getSortedRowModel`, … are no longer accepted; the table wires every row model itself. 3. **Custom aggregation functions** use TanStack v9's `constructAggregationFn({ aggregate })` shape. Built-in names (`"sum"`, `"mean"`, …) are unchanged. 4. **Instance APIs follow TanStack v9:** `column.pin("start" | "end")`, `column.getIsPinned()` returns `"start" | "end"`, `getPrePaginatedRowModel()`. Table options and `table.getState()` keep the `left`/`right` pinning shape, and `table.getState().columnSizingInfo` keeps its name. Other renames: `getLeft*`/`getRight*` are `getStart*`/`getEnd*` (leaf columns, visible cells, header groups, total size), `column.getStart("left")` and `column.getAfter("right")` take `"start"` / `"end"`, and `getPaginationRowModel()` is `getPaginatedRowModel()`. 5. **Option and column-def renames reach you too.** The column option `sortingFn` is now `sortFn`. `state.columnSizingInfo`, `initialState.columnSizingInfo` and `onColumnSizingInfoChange` are now `columnResizing` and `onColumnResizingChange` (only the value returned by `table.getState()` keeps the `columnSizingInfo` name). 6. **`filterFns`, `sortingFns` and `aggregationFns` options are gone.** The hook owns the feature set, so pass custom functions inline on the column (`filterFn: myFilterFn`, `sortFn: myCompare`, `aggregationFn: myAgg`). Built-in string names still work. Type inline filter functions with the exported `DataTableFilterFn`. 7. **Don't destructure row/column/cell methods** (`const { getValue } = row`); call them on the object. Cell-renderer `({ getValue })` still works. 8. **Row data must satisfy TanStack's `RowData`** (`Record | Array`). `unknown` is no longer accepted, so use a concrete row type for `useDataTable` and `DataTableInstance`. `DataTableInstance` with no type argument now defaults to `RowData`. 9. **Render contexts are unchanged.** Column renderers (`cell`, `header`, `footer`, `aggregatedCell`) still receive the enriched instance, so `({ table }) => table.getState()`, `table.dataTable` and the table's methods type-check. TanStack v9's own contexts don't include `getState`. 10. **`table.options` is the options object you passed in** on the latest render (TanStack v9's React adapter behavior), not TanStack's internally merged options with feature defaults. 11. **Two small TanStack features are registered by the block.** Their types add `dataTableInstanceFeature` and `dataTableUiFeature` through TanStack's `Plugins` / `Table_FeatureMap` / `TableState_FeatureMap` interfaces, scoped to tables built with the block's feature set. Other TanStack tables in your app are unaffected, except ones typed `Table`. 12. **The component holding the table no longer re-renders on table state changes.** If it reads the state while rendering (a selection count, the current density), add `stateSelector` for what it reads, or wrap that part in `table.Subscribe`. See [State management](/docs/guides/state#reading-state-in-your-component). ### API renames in 1.0.0 | 0.x | 1.0.0 | | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------- | | `table.tableInstance.setDensity(…)` (and every setter or action) | `table.setDensity(…)` | | `table.tableInstance.density` (and other UI state) | `table.getState().density` | | `table.tableInstance.isLoading` (and other options, `refs`) | `table.dataTable.isLoading` | | `DataTableConfig` type | `DataTableOptionsResolved` (+ `DataTableMethods`) | | `defaultDensity: "compact"` | `initialState: { density: "compact" }` | | `density`, `isFullscreen`, `showColumnFilters`, `advancedFilter`, `globalFilterMode` props | `state: { … }` | | `defaultShowColumnFilters`, `defaultGlobalFilterMode`, `defaultAdvancedFilter` | `initialState: { … }` | | `onEditCellSave` / `onSaveRow` / `onCreateRow` | `onEditingCellSave` / `onEditingRowSave` / `onCreatingRowSave` | | `renderToolbarActions` | `renderTopToolbarCustomActions` | | `meta.variant` / `meta.options` | `meta.filterVariant` / `meta.filterSelectOptions` | | `meta.disableHighlight: true` | `meta.enableFilterMatchHighlighting: false` | | `meta.disableColumnActions: true` | `meta.enableColumnActions: false` | | `` | `useDataTable({ pageSizeOptions: … })` | | `` | `useDataTable({ slotProps: { surface: { className: "…" } } })` | | `renderColumnActionsMenuItems` appended after a separator | return `<>{internalMenuItems}{yours}` | | Edit button next to the `renderRowActionMenuItems` menu | Edit is in the menu: render `internalMenuItems` in your items | | `sortingFns` option | `sortFns` | ## Related - [Installation](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/installation): The data table is distributed as a shadcn registry item. --- # Migrating from Material React Table The data table follows Material React Table (MRT) closely: the same hook-then-component shape, most of the same options, and the same TanStack Table underneath. Moving a table over is mostly renaming. This page lists what changes. ## The basics ```tsx // Material React Table const table = useMaterialReactTable({ columns, data }) return // This data table const table = useDataTable({ columns, data, getRowId: (row) => row.id }) return ``` - Type columns with `DataTableColumnDef` instead of `MRT_ColumnDef`. - Pass `getRowId` — row selection, editing and pinning key rows by id. - The table runs on **TanStack Table v9**, MRT on v8. The column options you share with TanStack (`accessorKey`, `cell`, `header`, `footer`, `sortFn`, `filterFn`, `enableSorting`, …) keep TanStack's names; v9 renames a few v8 ones, such as `sortingFn` → `sortFn`. ## Column options MRT puts its own column options on the column definition. Here they live under `meta`. | MRT column option | Here | | --- | --- | | `Cell` / `Header` / `Footer` | `cell` / `header` / `footer` (TanStack's) | | `filterVariant` | `meta.filterVariant` | | `filterSelectOptions` | `meta.filterSelectOptions` | | `filterFn` (a mode name) | `meta.filterMode` | | `columnFilterModeOptions` | `meta.columnFilterModeOptions` | | `enableColumnFilterModes` | `meta.enableColumnFilterModes` | | `Filter` | `meta.renderColumnFilter` | | `editVariant` / `editSelectOptions` | `meta.editVariant` / `meta.editSelectOptions` | | `enableEditing` | `meta.enableEditing` | | `Edit` | `meta.renderEditCell` | | `muiEditTextFieldProps` validation | `meta.validate` (return a message to block the save) | | `GroupedCell` / `AggregatedCell` / `PlaceholderCell` | `meta.renderGroupedCell` / `meta.renderAggregatedCell` / `meta.renderPlaceholderCell` | | `enableClickToCopy` | `meta.enableClickToCopy` | | `enableColumnActions` | `meta.enableColumnActions` | | `visibleInShowHideMenu` | `meta.visibleInShowHideMenu` | | `grow` | `meta.grow` (in `layoutMode: "grid"`, a weight: every data column grows by 1 unless set; see [Column resizing](/docs/guides/column-resizing#layout-mode-and-grow)) | | `muiTableHeadCellProps` / `muiTableBodyCellProps` / `muiTableFooterCellProps` | `meta.slotProps.headCell` / `.bodyCell` / `.footerCell` | ## Component props (`mui*Props`) MRT's `mui*Props` become one `slotProps` option. Each part takes props or a function of its context, and merges them with the table's own. See [Part props](/docs/guides/slot-props). | MRT | `slotProps` part | | --- | --- | | `muiTablePaperProps` | `root` | | `muiTableContainerProps` | `surface` | | `muiTableProps` | `table` | | `muiTableHeadProps` / `muiTableHeadRowProps` / `muiTableHeadCellProps` | `head` / `headRow` / `headCell` | | `muiFilterTextFieldProps` (cell) | `filterCell` | | `muiTableBodyProps` / `muiTableBodyRowProps` / `muiTableBodyCellProps` | `body` / `bodyRow` / `bodyCell` | | `muiDetailPanelProps` | `detailPanel` | | `muiTableFooterProps` / `muiTableFooterRowProps` / `muiTableFooterCellProps` | `footer` / `footerRow` / `footerCell` | | `muiTopToolbarProps` / `muiBottomToolbarProps` | `topToolbar` / `bottomToolbar` | | `muiPaginationProps` | `pagination` | | `muiToolbarAlertBannerProps` | `alertBanner` | | `muiSearchTextFieldProps` | `searchInput` | | `muiEditTextFieldProps` | `editField` | | `muiEditRowDialogProps` / `muiCreateRowModalProps` | `editDialog` (`isCreating` tells them apart) | | `muiSelectCheckboxProps` | `selectCheckbox` | ## State | MRT state | Here | | --- | --- | | `isFullScreen` | `isFullscreen` | | `columnFilterFns` | `columnFilterModes` | | `globalFilterFn` | `globalFilterMode` | | `editingRow` | `editingRowId` (the draft is in `rowDraft`) | | `editingCell` | `editingCell` (`{ rowId, columnId }`) | | `creatingRow` | `isCreating` (the draft is in `rowDraft`) | | `isLoading` / `isSaving` / `showProgressBars` / `showSkeletons` / `showLoadingOverlay` | options of the same names, not state | | `density`, `showColumnFilters`, and TanStack's own state | unchanged | Set and control state the same way: `initialState`, `state`, and the matching `on…Change` callback (`onIsFullScreenChange` → `onIsFullscreenChange`, `onColumnFilterFnsChange` → `onColumnFilterModesChange`, `onGlobalFilterFnChange` → `onGlobalFilterModeChange`). See [State management](/docs/guides/state). ## Table options Most options keep their MRT names: `enableRowSelection`, `enableMultiRowSelection`, `enableBatchRowSelection`, `enableSelectAll`, `selectAllMode`, `enableEditing`, `editDisplayMode`, `createDisplayMode`, `positionCreatingRow`, `enableGrouping`, `enableColumnPinning`, `enableRowPinning`, `enableColumnOrdering`, `enableColumnResizing`, `enableRowOrdering`, `enableRowNumbers`, `enableRowVirtualization`, `enableColumnVirtualization`, `rowVirtualizerOptions`, `columnVirtualizerOptions`, `enableStickyHeader`, `enableStickyFooter`, `enableTableHead`, `enableTableFooter`, `enableExpandAll`, `enableColumnFilterModes`, `enableGlobalFilterModes`, `enableGlobalFilterRankedResults`, `enableFacetedValues`, `enableFilterMatchHighlighting`, `enableClickToCopy`, `enableCellActions`, `enableDensityToggle`, `enableFullScreenToggle` → `enableFullscreenToggle`, `enableTopToolbar`, `enableBottomToolbar`, `enableToolbarInternalActions`, `columnFilterDisplayMode`, `paginationDisplayMode`, `positionPagination`, `positionGlobalFilter`, `positionActionsColumn`, `positionExpandColumn`, `positionToolbarAlertBanner`, `positionToolbarDropZone`, `displayColumnDefOptions`, `defaultDisplayColumn`, `filterFns`, `aggregationFns`, `columnFilterModeOptions`, `globalFilterModeOptions`, `localization`, `icons`. Renamed or reshaped: | MRT | Here | | --- | --- | | `sortingFns` | `sortFns` (TanStack v9's name) | | `rowNumberDisplayMode` | `rowNumberMode` | | `renderEmptyRowsFallback` | `renderEmpty` | | `muiPaginationProps.rowsPerPageOptions` | `pageSizeOptions` | | `state.isLoading` and friends | the `isLoading`, `isSaving`, `showProgressBars`, `showSkeletons`, `showLoadingOverlay` options | | `onEditingRowSave({ values, table })`, then `table.setEditingRow(null)` | `onEditingRowSave({ row, values, table, exit })`, then `exit()` | | `onCreatingRowSave({ values, table })`, then `table.setCreatingRow(null)` | `onCreatingRowSave({ values, table, exit })`, then `exit()` | | cell edits saved from `muiEditTextFieldProps.onBlur` | `onEditingCellSave({ row, column, value, table })` | | `displayColumnDefOptions["mrt-row-select"]` (and the other `mrt-row-*` ids) | `displayColumnDefOptions["cn-select"]` — ids: `cn-select`, `cn-expand`, `cn-row-actions`, `cn-row-number`, `cn-row-pin`, `cn-row-drag` | Callbacks and render options with the same name and arguments: `onEditingRowCancel`, `onCreatingRowCancel`, `renderTopToolbarCustomActions`, `renderBottomToolbarCustomActions`, `renderToolbarInternalActions`, `renderTopToolbar`, `renderBottomToolbar`, `renderRowActions`, `renderDetailPanel`, `renderCaption`, `renderEditRowDialogContent`, `renderCreateRowDialogContent`, `renderColumnFilterModeMenuItems`, `renderGlobalFilterModeMenuItems`, and the menu callbacks `renderColumnActionsMenuItems`, `renderRowActionMenuItems`, `renderCellActionMenuItems` (each receives `internalMenuItems` and `closeMenu`). ## The table instance | MRT | Here | | --- | --- | | `table.setEditingRow(row)` | `table.beginRowEdit(row)` | | `table.setEditingRow(null)` / `table.setCreatingRow(null)` | `table.cancelEdit()` | | `table.setCreatingRow(true)` | `table.beginCreate()` | | `table.setEditingCell(cell)` | `table.setEditingCell({ rowId, columnId })` | | `table.setIsFullScreen(…)` | `table.setIsFullscreen(…)` | | `table.setColumnFilterFns(…)` | `table.setColumnFilterMode(columnId, mode)` | | `table.setGlobalFilterFn(…)` | `table.setGlobalFilterMode(mode)` | | `table.refs.tableContainerRef` (and the other refs) | `table.dataTable.refs.tableContainerRef` | | `table.options.enableEditing` (and MRT's other options) | `table.dataTable.enableEditing` | | `table.getState()` | unchanged, including the UI state above | The instance is stable and the component that calls `useDataTable` does not re-render on state changes. Read state where you render it with `table.Subscribe`, or pass `stateSelector`. See [State management](/docs/guides/state). ## Defaults that differ | Option | MRT | Here | Why | | --- | --- | --- | --- | | `editDisplayMode` | `"modal"` | `"cell"` | Inline editing is the quickest edit for most tables | | `positionActionsColumn` | `"first"` | `"last"` | Actions read best after the data they act on | | `enableStickyHeader` | `false` | `true` | Headers stay useful on long tables | | `enableFacetedValues` | `false` | `true` | Select filters fill their options from the data | | `enableGlobalFilterRankedResults` | `true` | `false` | Searching never reorders rows unless you opt in | | Text filter mode | `"fuzzy"` | `"contains"` | Predictable matches; fuzzy is one menu click away | | Cell editing starts on | double-click | single click | One click to edit, matching the click-to-edit affordance | Set the MRT value explicitly to keep the old behavior. ## Not here (yet) - `rowPinningDisplayMode` values `"sticky"` and `"select-sticky"` (rows that stick in place); use `"top-and-bottom"` or `"select-top"`. - `enableColumnDragging` as a separate switch. - MRT's `enableRowDragging` works through `DataTableDndProvider` and `onRowDragEnd` here; see [Row dragging](/docs/guides/row-dragging). - MUI theming: the table uses your shadcn/ui components and theme instead. ## Related - [Updating](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/updating): shadcn add copies this block's source into your project — after that, the code is yours, and upstream releases don't reach it automatically. - [Part props (slotProps)](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/slot-props): Every part of the table — the scroll surface, rows, cells, toolbars, the search box, edit fields and more — accepts extra props through the slotProps option. - [State management](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/state): The data table keeps all of its state in TanStack Table's store: the standard table state (sorting, filters, selection…) and its own UI state (density, editing, filter modes…). --- # Column filtering Each column can expose its own filter control in a per-column filter row. Toggle the row with the funnel icon in the toolbar, or have it open from the start with `initialState: { showColumnFilters: true }`. Example (`column-filters`): ```tsx function ColumnFiltersExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { showColumnFilters: true, pagination: { pageSize: 10 } }, }) return } ``` The filter-mode menu and a clear button sit inside the input. The menu leads and the clear button shows while there is a value. Range filters have one clear button per side. Dropdown, checkbox and slider filters keep the menu beside the control. ## Choosing a filter control The UI for each filter is chosen by the column's `meta.filterVariant`: | Variant | Control | Values | | --- | --- | --- | | `"text"` (default) | Text box | Any | | `"autocomplete"` | Text box suggesting the column's values | Any | | `"select"` / `"multi-select"` | Dropdown / checklist | Strings | | `"checkbox"` | Checkbox | Booleans | | `"range"` / `"range-slider"` | Number boxes / slider | Numbers | | `"date"` / `"date-range"` | Calendar | Dates, compared by day | | `"datetime"` / `"datetime-range"` | Date-time box | Dates, compared to the minute | | `"time"` / `"time-range"` | Time box | `"HH:mm"` strings or Dates, compared by time of day | The date, date-time and time variants share the same modes (equals, before, after, between, empty…); the `-range` variants start in "between". For `select`, `multi-select` and `autocomplete`, supply `meta.filterSelectOptions` as an array of `{ label, value }`. If you omit them, the options are derived from the column's faceted unique values. ```tsx import { DataTable, useDataTable } from "@/components/ui/data-table" const columns = [ { accessorKey: "title", header: "Title", meta: { filterVariant: "text" } }, { accessorKey: "status", header: "Status", meta: { filterVariant: "select", filterSelectOptions: [ { label: "Active", value: "active" }, { label: "Archived", value: "archived" }, ], }, }, { accessorKey: "score", header: "Score", meta: { filterVariant: "range" } }, ] const table = useDataTable({ data, columns, getRowId: (row) => row.id }) return ``` Example (`datetime-filters`): ```tsx function DateTimeFiltersExample() { const table = useDataTable({ data: SHIFTS, columns: SHIFT_COLUMNS, getRowId: (row) => row.id, initialState: { showColumnFilters: true, pagination: { pageSize: 10 } }, }) return } ``` ## Open the filter row by default ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { showColumnFilters: true }, }) ``` ## Custom filter UI Use `meta.renderColumnFilter` as an escape hatch to render your own filter control for a column. ## Match highlighting Matched substrings in cells are highlighted by default. Disable it for the whole table with `enableFilterMatchHighlighting: false`, or for a single column with `meta.enableFilterMatchHighlighting: false`. A column with its own `cell` renderer gets the highlighted value as `renderedCellValue` (as in Material React Table). Render it where the value goes to keep the highlighting: ```tsx { accessorKey: "team", header: "Team", cell: ({ renderedCellValue }) => {renderedCellValue}, } ``` ## Disabling a column's filter Set `enableColumnFilter: false` on a column to remove its filter control entirely. ## Faceted values Select / multi-select filters auto-derive their options, and range filters their min/max bounds, from the data's faceted values. This is on by default; set `enableFacetedValues: false` to skip computing the faceted row models (e.g. for very large client-side datasets) — then provide `meta.filterSelectOptions` explicitly for select-style filters. Example (`faceted-values`): ```tsx function FacetedValuesExample() { const data = React.useMemo(() => baseUsers, []) const table = useDataTable({ data, columns: FACETED_COLUMNS, getRowId: (row) => row.id, initialState: { showColumnFilters: true, pagination: { pageIndex: 0, pageSize: 10 }, }, }) return } ``` ## Relevant options `useDataTable` options that affect column filtering: | Name | Type | Default | Description | | --- | --- | --- | --- | | `onShowColumnFiltersChange` | `(showColumnFilters: boolean) => void` | | Called whenever the filter row is shown or hidden. Set it with `initialState.showColumnFilters`, control it with `state.showColumnFilters`. | | `enableColumnFilterModes` | `boolean` | `true` | Show the filter-mode menu adornment on filter fields. Default true. | | `enableFilterMatchHighlighting` | `boolean` | `true` | Highlight matched substrings in cells. Default true. | | `enableFacetedValues` | `boolean` | `true` | Compute faceted unique values / min-max (auto select options + range bounds). Default true; disable to skip the faceted row models. | | `columnFilterDisplayMode` | `ColumnFilterDisplayMode` | `"subheader"` | Where column filter inputs live: `"subheader"` (a filter row under the header, default), `"popover"` (per-column popovers opened from the column header), or `"custom"` (you render them). | Per-column, via `columnDef.meta`: | meta key | Type | Default | Description | | --- | --- | --- | --- | | `filterVariant` | `FilterVariant` | | Filter UI variant rendered in the filter row. Defaults to "text". | | `filterSelectOptions` | `DataTableFilterOption[]` | | Options for `select` / `multi-select` filter variants. If omitted for a select-style variant, options are derived from faceted unique values. | | `filterMode` | `FilterMode` | | Default filter mode for this column (overrides the per-variant default). | | `enableColumnFilterModes` | `boolean` | | Per-column override for the filter-mode menu (defaults to the table). | | `renderColumnFilter` | `(props: { column: Column table: DataTableInstance }) => React.ReactNode` | | Custom filter UI for this column (escape hatch). Replaces the variant. | | `columnFilterModeOptions` | `FilterMode[]` | | Restrict (and order) the filter-mode menu for this column to this subset of modes (built-in ones and custom `filterFns` names). Include the column's default mode. | | `enableFilterMatchHighlighting` | `boolean` | | `false` opts this column out of match highlighting (the table option of the same name turns it on or off for every column). | ## Related - [Filter modes](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/filter-modes): Filter modes let users change how a column's filter is applied — for example matching with Contains, Starts with, Equals, Between, Greater than, or Empty. - [Display modes](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/display-modes): Several features expose a *DisplayMode option that changes _how_ the UI is presented without changing the underlying behavior. - [Column options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/column-options): Per-column configuration lives on the standard TanStack columnDef.meta object, which the table augments with the fields below (fully typed wherever meta is read). --- # Filter modes Filter modes let users change how a column's filter is applied — for example matching with Contains, Starts with, Equals, Between, Greater than, or Empty. With `enableColumnFilterModes: true` (the default) each filter field shows a mode menu offering the modes appropriate to that column's `meta.filterVariant`. Example (`filter-modes`): ```tsx function FilterModesExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableColumnFilterModes: true, initialState: { showColumnFilters: true, pagination: { pageSize: 10 } }, }) return } ``` ## Setting the initial mode Set a column's starting mode with `meta.filterMode`. ```tsx import { DataTable, useDataTable } from "@/components/ui/data-table" const columns = [ { accessorKey: "title", header: "Title", meta: { filterVariant: "text", filterMode: "startsWith" }, }, { accessorKey: "score", header: "Score", meta: { filterVariant: "range", filterMode: "between" }, }, ] const table = useDataTable({ data, columns, getRowId: (row) => row.id }) return ``` ## Toggling the mode menu `enableColumnFilterModes` is `true` by default. Turn it off globally to hide every mode menu, or override it per column with `meta.enableColumnFilterModes`. ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableColumnFilterModes: false, }) ``` ```tsx const columns = [ // mode menu hidden for this column only { accessorKey: "id", header: "ID", meta: { enableColumnFilterModes: false } }, ] ``` ## Restricting the available modes Limit a column's mode menu to a specific subset (and order) with `meta.columnFilterModeOptions`. Only modes valid for the column's variant are kept (plus any [custom functions](#custom-filter-functions)); include the column's default mode. ```tsx const columns = [ { accessorKey: "title", header: "Title", meta: { filterVariant: "text", filterMode: "contains", columnFilterModeOptions: ["contains", "startsWith", "equals"], }, }, ] ``` To restrict every column at once, pass `columnFilterModeOptions` to `useDataTable`. Each column keeps the modes its variant supports; a column's own `meta.columnFilterModeOptions` wins. ```tsx const table = useDataTable({ data, columns, columnFilterModeOptions: ["contains", "equals", "between", "betweenInclusive"], }) ``` ## Custom filter functions Add your own filter functions with the `filterFns` option. Each one takes TanStack's filter function arguments — `(row, columnId, filterValue)` — and returns whether the row passes. Then use it by name: - in a mode menu, by listing it in `meta.columnFilterModeOptions` or `columnFilterModeOptions`; - as a column's starting mode, with `meta.filterMode`; - as a column's fixed filter (no mode menu), with `filterFn`; - in the global search, by listing it in `globalFilterModeOptions` — see [Global search](/docs/guides/global-search#custom-search-modes). ```tsx const table = useDataTable({ data, columns: [ { accessorKey: "name", header: "Name", meta: { columnFilterModeOptions: ["contains", "startsWith", "wordStartsWith"], }, }, ], filterFns: { wordStartsWith: (row, columnId, value) => String(row.getValue(columnId)) .toLowerCase() .split(/\s+/) .some((word) => word.startsWith(String(value).toLowerCase())), }, localization: { filterModes: { wordStartsWith: "Any word starts with" } }, }) ``` The menu labels a custom function with `localization.filterModes[name]`, or the name itself when there is no label. The names are read when the table is created, so define the functions outside your component (or memoize them). Example (`custom-filter-fns`): ```tsx function CustomFilterFnsExample() { const data = React.useMemo(() => baseUsers, []) const table = useDataTable({ data, columns: NAME_COLUMNS, getRowId: (row) => row.id, initialState: { showColumnFilters: true, pagination: { pageSize: 10 }, }, // Matches when any word of the cell starts with the typed text. filterFns: { wordStartsWith: (row, columnId, value) => String(row.getValue(columnId)) .toLowerCase() .split(/\s+/) .some((word) => word.startsWith(String(value).toLowerCase())), }, localization: { filterModes: { wordStartsWith: "Any word starts with" } }, }) return } ``` ## Custom mode menu Replace the built-in radio items with your own using `renderColumnFilterModeMenuItems`. You get the allowed `modes`, the `currentMode`, and an `onSelect(mode)` to switch. ```tsx import { DropdownMenuItem } from "@/components/ui/dropdown-menu" const table = useDataTable({ data, columns, getRowId: (row) => row.id, renderColumnFilterModeMenuItems: ({ modes, currentMode, onSelect }) => modes.map((mode) => ( onSelect(mode)} > {mode} )), }) ``` The global-search mode menu has the same hook: `renderGlobalFilterModeMenuItems` — see [Global search](/docs/guides/global-search). ## Localizing mode labels Mode menu labels are localizable via `localization.filterModes`. Labels you pass are merged with the built-in ones, so you only need to list the modes you change or add. ## Related - [Column filtering](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/column-filtering): Each column can expose its own filter control in a per-column filter row. - [Localization](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/localization): Every user-facing string flows through a localization table. --- # Global search An expandable global search box appears in the toolbar by default (`enableGlobalFilter: true`). It filters across all searchable columns at once, complementing the per-column filters. Example (`global-search`): ```tsx function GlobalSearchExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableGlobalFilter: true, initialState: { globalFilterMode: "fuzzy", pagination: { pageSize: 10 } }, }) return } ``` ## Search modes When `enableGlobalFilterModes: true` (the default), the search icon at the start of the box opens a mode menu — fuzzy, contains, and more. A clear button sits at the end of the box while it has text. The initial mode comes from `initialState.globalFilterMode`, which defaults to `"fuzzy"`. ```tsx import { DataTable, useDataTable } from "@/components/ui/data-table" const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableGlobalFilter: true, enableGlobalFilterModes: true, initialState: { globalFilterMode: "fuzzy" }, }) return ``` ## Opening the search box The search box starts closed unless a search is set. The search button opens it and stays beside it; clicking the button again closes the box and clears the search. Its open state is table state, `showGlobalFilter`: start it open with `initialState: { showGlobalFilter: true }`, control it with `state.showGlobalFilter` and `onShowGlobalFilterChange`, or open it from code with `table.setShowGlobalFilter(true)`. Setting a search value from outside opens it too. ## Custom search modes Limit or reorder the search mode menu with `globalFilterModeOptions`. It can also list your own [custom filter functions](/docs/guides/filter-modes#custom-filter-functions) by name; the search runs them on each searchable column, and a row matches when any column passes. ```tsx const table = useDataTable({ data, columns, filterFns: { exact: (row, columnId, value) => String(row.getValue(columnId)) === value, }, globalFilterModeOptions: ["contains", "fuzzy", "exact"], initialState: { globalFilterMode: "contains" }, localization: { filterModes: { exact: "Exact match" } }, }) ``` ## Disabling global search Set `enableGlobalFilter: false` to remove the search box from the toolbar. ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableGlobalFilter: false, }) ``` ## Custom mode menu Replace the built-in mode radio items with `renderGlobalFilterModeMenuItems`. It receives the allowed `modes`, the `currentMode`, and an `onSelect(mode)` to switch. ```tsx import { DropdownMenuItem } from "@/components/ui/dropdown-menu" useDataTable({ renderGlobalFilterModeMenuItems: ({ modes, currentMode, onSelect }) => modes.map((mode) => ( onSelect(mode)} > {mode} )), }) ``` ## Ranked results In fuzzy mode, each match carries a relevance score. Set `enableGlobalFilterRankedResults` to order rows by that score — best matches first — for as long as a search is active. ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { globalFilterMode: "fuzzy" }, enableGlobalFilterRankedResults: true, }) ``` The ranking yields to the user: the moment they click a column to sort, their sort takes over, and clearing the search restores the original order. It applies only in `"fuzzy"` mode and is skipped while grouping or row expansion is active, or under `manualSorting` / `manualFiltering` (the server owns ordering there). > **Off by default:** Material React Table enables this by default; the data table keeps it off so adding a > search box never silently reorders your rows. Opt in when relevance ordering > is what you want. ## Placement Move the search to the left toolbar region with `positionGlobalFilter: "left"` — see [Toolbar customization](/docs/guides/toolbar#search-placement). ## Relevant options | Name | Type | Default | Description | | --- | --- | --- | --- | | `enableGlobalFilter` | `boolean` | `true` | Show the expandable global search in the toolbar. Default true. | | `enableGlobalFilterModes` | `boolean` | `true` | Show the global search mode menu (fuzzy/contains/…). Default true. | | `onShowGlobalFilterChange` | `(showGlobalFilter: boolean) => void` | | Called whenever the global search box opens or closes. Set it with `initialState.showGlobalFilter`, control it with `state.showGlobalFilter`. | | `globalFilterModeOptions` | `GlobalFilterMode[]` | `GLOBAL_FILTER_MODES as GlobalFilterMode[]` | Restrict (and order) the global search mode menu: built-in modes and custom `filterFns` names. | | `enableGlobalFilterRankedResults` | `boolean` | `false` | While the global search is in fuzzy mode, order rows by best match (most relevant first) until the user applies their own sort. Default false — MRT defaults this on, but the data table keeps it off so searching never silently reorders rows unless opted in. Ignored for non-fuzzy modes, when grouping or expanded, or under manual sorting/filtering. | | `onGlobalFilterModeChange` | `(mode: GlobalFilterMode) => void` | | Called whenever the global search mode changes (default `"fuzzy"`). Set it with `initialState.globalFilterMode`, control it with `state.globalFilterMode`. | | `positionGlobalFilter` | `"left" \| "right" \| "none"` | `"right"` | Which toolbar region the global search renders in. Default "right" (the internal-actions cluster). "left" places it next to the title/actions; "none" hides it (same as `enableGlobalFilter: false`). | ## Related - [Column filtering](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/column-filtering): Each column can expose its own filter control in a per-column filter row. - [Filter modes](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/filter-modes): Filter modes let users change how a column's filter is applied — for example matching with Contains, Starts with, Equals, Between, Greater than, or Empty. --- # Advanced filter Set `enableAdvancedFilter: true` to add a toolbar button that opens a dialog where users build compound filter rules — multiple conditions joined by **All** (AND) or **Any** (OR) logic. The button badges the number of active rules. Example (`advanced-filter`): ```tsx function AdvancedFilterExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableAdvancedFilter: true, initialState: { pagination: { pageSize: 10 } }, }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableAdvancedFilter: true, }) return ``` ## How it works Each rule is a `{ columnId, operator, value }`. The available operators adapt to the column's `meta.filterVariant`: | Variant | Operators | | ------------------------- | ---------------------------------------------------------------------------------------------- | | `text` (default) | contains, does not contain, starts with, ends with, equals, not equals, is empty, is not empty | | `select` / `multi-select` | equals, not equals, is empty, is not empty | | `range` / `range-slider` | equals, not equals, `>`, `≥`, `<`, `≤`, between, is empty, is not empty | | `date` / `date-range` | equals, not equals, `>`, `<`, between, is empty, is not empty | | `checkbox` | equals | Build your rules in the dialog, then click **Apply** to filter the table. Closing or cancelling the dialog discards unapplied edits. > **Additive to column filters:** The advanced filter is independent of the per-column filter row and global > search: a row must pass all active filter systems to be shown. Clear > the panel's rules (or use "Clear all") to remove just the advanced filters. ## Controlled usage Drive the filter from your own state with `state.advancedFilter` + `onAdvancedFilterChange` (or seed an uncontrolled table with `initialState.advancedFilter`). ```tsx const [filter, setFilter] = React.useState({ logic: "and", rules: [], }) const table = useDataTable({ data, columns, enableAdvancedFilter: true, state: { advancedFilter: filter }, onAdvancedFilterChange: setFilter, }) ``` ## Related - [Column filtering](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/column-filtering): Each column can expose its own filter control in a per-column filter row. - [Filter modes](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/filter-modes): Filter modes let users change how a column's filter is applied — for example matching with Contains, Starts with, Equals, Between, Greater than, or Empty. - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. --- # Column visibility A "Columns" toggle in the toolbar lets users show or hide columns. Each column-actions menu also has a Hide item for the same effect. Example (`column-visibility`): ```tsx function ColumnVisibilityExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { columnVisibility: { startDate: false, progress: false }, pagination: { pageSize: 10 }, }, }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, }) return ``` ## Initial hidden columns Hide columns from the start with `initialState.columnVisibility`, mapping a column id to `false`. ```tsx const table = useDataTable({ data, columns, initialState: { columnVisibility: { startDate: false }, }, }) ``` ## Prevent hiding a column Use TanStack's `enableHiding: false` on a column definition to keep it always visible. It will not appear in the toggle or expose a Hide item. ```tsx const columns = [ { accessorKey: "firstName", enableHiding: false }, ] ``` > **Always-on columns:** Columns with `enableHiding: false` stay rendered regardless of toolbar or menu actions. ## Leaving a column out of the menu Set `meta.visibleInShowHideMenu: false` to keep a column out of the show / hide columns menu while still letting it be hidden from code (`column.toggleVisibility`). ## Related - [Density](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/density): Density controls the vertical padding of table rows and the padding around the toolbars. - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. - [Column options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/column-options): Per-column configuration lives on the standard TanStack columnDef.meta object, which the table augments with the fields below (fully typed wherever meta is read). --- # Column ordering Set `enableColumnOrdering: true` to add a drag grip to each header. Users can grab the grip and drag a column to a new position. Example (`column-ordering`): ```tsx function ColumnOrderingExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableColumnOrdering: true, initialState: { pagination: { pageSize: 10 } }, }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableColumnOrdering: true, }) return ``` ## While you drag The whole column moves with its header — body, filter and footer cells included — and the other columns slide aside to show where it will land. The movement comes from CSS variables on the scroll surface, so dragging doesn't re-render any rows. With "reduce motion" turned on in the operating system, the columns move without the slide animation. Example (`column-drag`): ```tsx function ColumnDragExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, // Drag a header's grip: the whole column follows and the others make room. enableColumnOrdering: true, enableColumnPinning: true, initialState: { pagination: { pageIndex: 0, pageSize: 8 } }, }) return } ``` ## Initial order Provide the starting order with `initialState.columnOrder`, an array of column ids. ```tsx const table = useDataTable({ data, columns, enableColumnOrdering: true, initialState: { columnOrder: ["firstName", "lastName", "salary"], }, }) ``` > **Pair with pinning:** Pinned columns stay anchored while the rest reorder. Combine ordering with pinning to keep key columns in view. ## Related - [Column pinning](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/column-pinning): Set enableColumnPinning: true to let users pin columns left or right from the column-actions menu. - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. - [Column options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/column-options): Per-column configuration lives on the standard TanStack columnDef.meta object, which the table augments with the fields below (fully typed wherever meta is read). --- # Column pinning Set `enableColumnPinning: true` to let users pin columns left or right from the column-actions menu. Pinned columns stay sticky while the table scrolls horizontally, with a solid line and a soft shadow on their inner edge. Example (`column-pinning`): ```tsx function ColumnPinningExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableColumnPinning: true, initialState: { columnPinning: { left: ["firstName"], right: ["salary"] }, pagination: { pageSize: 10 }, }, }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableColumnPinning: true, }) return ``` ## Initial pins Pin columns from the start with `initialState.columnPinning`, grouping ids into `left` and `right`. ```tsx const table = useDataTable({ data, columns, enableColumnPinning: true, initialState: { columnPinning: { left: ["firstName"], right: ["salary"], }, }, }) ``` Table options and `table.getState()` use `left` / `right`, while instance methods such as `column.pin` and `column.getIsPinned` use TanStack v9's `start` / `end`. > **Sticky positioning:** Pinned columns rely on the same sticky behavior used elsewhere in the table. See the sticky guide for how that interacts with headers and footers. ## Related - [Column ordering](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/column-ordering): Set enableColumnOrdering: true to add a drag grip to each header. - [Sticky](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/sticky): The header row (enableStickyHeader, default true) sticks to the top of the scroll surface. - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. --- # Column resizing Set `enableColumnResizing: true` to add a drag handle on each column's edge. Users drag the handle to widen or narrow a column. Example (`column-resizing`): ```tsx function ColumnResizingExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableColumnResizing: true, initialState: { pagination: { pageSize: 10 } }, }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableColumnResizing: true, }) return ``` ## Keyboard Each resize handle is focusable. With it focused, the arrow keys widen or narrow the column by 10px (50px with Shift) — mirrored in right-to-left tables — Home and End jump to the column's `minSize` and `maxSize`, and Enter fits the column to its content (or resets it when auto-size is off). Screen readers hear it as a separator with the column's current width. ## Resize mode The resize mode defaults to `"onChange"`, so columns resize live as the handle moves. Pass TanStack's `columnResizeMode: "onEnd"` to defer the resize until the handle is released. ```tsx const table = useDataTable({ data, columns, enableColumnResizing: true, columnResizeMode: "onEnd", }) ``` ## Width constraints Column widths come from `size`, `minSize`, and `maxSize` on the column definition. ```tsx const columns = [ { accessorKey: "firstName", size: 200, minSize: 120, maxSize: 320 }, ] ``` > **Set sensible bounds:** Define `minSize` and `maxSize` so users cannot collapse or stretch a column > past a usable width. ## Auto-sizing Double-click a column's resize handle to size it to fit its widest value — header and data, across the full dataset (not just the rows currently on screen). This is on by default whenever `enableColumnResizing` is set, and respects each column's `minSize`/`maxSize` bounds. Set `enableColumnAutosize: false` to turn it off; double-clicking the handle then resets the column to its default size instead. ```tsx const table = useDataTable({ data, columns, enableColumnResizing: true, enableColumnAutosize: true, // default: matches enableColumnResizing }) ``` You can also trigger it imperatively from the table instance: ```tsx table.autoSizeColumn("firstName") table.autoSizeAllColumns() ``` Example (`column-autosize`): ```tsx function ColumnAutosizeExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableColumnResizing: true, enableColumnAutosize: true, initialState: { pagination: { pageSize: 10 } }, renderTopToolbarCustomActions: ({ table }) => ( ), }) return } ``` ## Layout mode and grow `layoutMode` decides how the table hands out its width, as in Material React Table: - `"semantic"` (default) — the browser's table layout. Columns with a `size` keep it and the others share what's left. - `"grid"` — every column keeps its `size`, and the spare width (the container's width minus the columns' sizes) goes to the growing columns, so the table fills its container. When the columns are wider than the container, nothing grows and the table scrolls. - `"grid-no-grow"` — every column keeps its `size` and nothing grows; the table is exactly as wide as its columns. In grid mode every data column grows by default. Set `meta.grow` on a column to change that: `false` (or `0`) keeps it at its size, and a number sets its share — a column with `grow: 2` takes twice the spare width of one with `grow: 1`. The table's own columns (selection, expand, row numbers, row drag, row pin, row actions) never grow. ```tsx const columns = [ { accessorKey: "firstName", size: 150 }, { accessorKey: "email", size: 220, meta: { grow: 2 } }, { accessorKey: "role", size: 130, meta: { grow: false } }, ] const table = useDataTable({ data, columns, layoutMode: "grid", enableColumnResizing: true, }) ``` Resizing works in every mode. In grid mode, resizing a growing column starts from the width it shows, and from then on the column keeps the size you gave it (it has a size in `columnSizing`, so it stops growing); the other growing columns take up the difference. Resetting the column (`column.resetSize()`) lets it grow again. A column given a size through `initialState.columnSizing` does not grow either. Example (`layout-mode`): ```tsx function LayoutModeExample() { const data = React.useMemo(() => baseUsers, []) const [layoutMode, setLayoutMode] = React.useState("grid") const table = useDataTable({ data, columns: layoutModeColumns, getRowId: (row) => row.id, layoutMode, enableColumnResizing: true, enableRowSelection: true, initialState: { pagination: { pageSize: 5 } }, renderTopToolbarCustomActions: () => (
{LAYOUT_MODES.map((mode) => ( ))}
), }) return } ``` ## Related - [Column options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/column-options): Per-column configuration lives on the standard TanStack columnDef.meta object, which the table augments with the fields below (fully typed wherever meta is read). - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. --- # Row selection Set `enableRowSelection: true` to add a leading checkbox column. When rows are selected, a selection alert banner appears showing the current count. `enableRowSelection` accepts a boolean or a `(row) => boolean` predicate (standard TanStack), so you can allow selecting only certain rows. > **Stable selection keys:** Always provide `getRowId` so selection keys stay stable across re-renders, > sorting, and pagination. Without it, selection is keyed by row index and can > drift. ## row-selection Multi-row selection with a select-all checkbox in the header. Example (`row-selection`): ```tsx function RowSelectionExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowSelection: true, initialState: { pagination: { pageSize: 10 } }, }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowSelection: true, }) return ``` Read the current selection at any time. To show it in your own component's render, subscribe to it with `stateSelector` (the component holding the table does not re-render on table changes by default; see [State management](/docs/guides/state#reading-state-in-your-component)): ```tsx const table = useDataTable({ // … enableRowSelection: true, stateSelector: (state) => state.rowSelection, }) const selected = table.getState().rowSelection ``` Or control it from your own state: ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowSelection: true, state: { rowSelection }, onRowSelectionChange: setRowSelection, }) ``` You can also gate selection per row with a predicate: ```tsx enableRowSelection: (row) => row.original.status !== "archived" ``` ## single-selection For single selection, add `enableMultiRowSelection: false`. Checking a row unchecks the one before it, and the select-all checkbox goes away. Example (`single-selection`): ```tsx function SingleSelectionExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowSelection: true, enableMultiRowSelection: false, initialState: { pagination: { pageSize: 10 } }, }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowSelection: true, enableMultiRowSelection: false, }) return ``` ## Selecting a range Click one row's checkbox, then shift-click another: every row between them takes the same state — selected, or cleared if the shift-clicked row was selected. Ranges follow the rows as displayed (sorted and filtered). Turn it off with `enableBatchRowSelection: false`. Example (`selection-actions`): ```tsx function SelectionActionsExample() { const [data, setData] = React.useState(() => baseUsers.slice(0, 20)) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowSelection: true, initialState: { pagination: { pageSize: 10 } }, renderTopToolbarCustomActions: ({ table }) => ( state.rowSelection}> {(selection) => { const count = Object.keys(selection).length return ( ) }} ), }) return } ``` ## Select-all behavior The selection column header carries a select-all checkbox. `selectAllMode` controls its scope — `"page"` (default) toggles the current page, `"all"` toggles every row across pages. Set `enableSelectAll: false` to hide the header checkbox entirely. ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowSelection: true, selectAllMode: "all", // "page" | "all" enableSelectAll: true, }) ``` ## Where the selection banner shows `positionToolbarAlertBanner` places the banner with the selected count and **Clear selection**: `"top"` (default, between the toolbar and the table), `"bottom"` (under the table), `"head-overlay"` or `"none"`. With `"head-overlay"` (as in Material React Table) the banner lies over the header row instead, with the select-all checkbox at its start, so selecting rows never pushes the table down. It stays over a sticky header while the table scrolls, spans the visible width when it scrolls sideways, and goes away when nothing is selected, leaving the header clickable again. If you Tab into a header control while it shows, it steps aside until focus leaves the header row. It needs the table head (`enableTableHead`). Example (`selection-banner-overlay`): ```tsx function SelectionBannerOverlayExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowSelection: true, positionToolbarAlertBanner: "head-overlay", initialState: { pagination: { pageSize: 10 } }, }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowSelection: true, positionToolbarAlertBanner: "head-overlay", // "top" | "bottom" | "head-overlay" | "none" }) ``` ## Relevant options | Name | Type | Default | Description | | --- | --- | --- | --- | | `enableSelectAll` | `boolean` | `true` | Show the select-all checkbox in the selection column header. Default true. | | `selectAllMode` | `"page" \| "all"` | `"page"` | Select-all scope for the header checkbox: the current page ("page", default) or every row ("all"). | | `enableBatchRowSelection` | `boolean` | `true` | Shift-click a row's checkbox to select (or clear) every row since the last one clicked. Default true. | | `positionToolbarAlertBanner` | `"top" \| "bottom" \| "head-overlay" \| "none"` | `"top"` | Where the row-selection alert banner renders: above the table (`"top"`, default), below it (`"bottom"`), over the header row (`"head-overlay"`, with the select-all checkbox, so selecting rows does not move the table; needs the table head), or nowhere (`"none"`). | ## Related - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. - [Table instance](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/table-instance): useDataTable returns the standard TanStack table instance with three additions: the table's UI state in table.getState() (density, editing, filter modes…), methods to change it on table itself, and the resolved options and DOM refs on table.dataTable. - [Export](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/export): The table ships no built-in export — the same stance as Material React Table — so the block carries no CSV or spreadsheet dependencies. --- # Row ordering Set `enableRowOrdering: true` to add a drag-handle column for drag-and-drop row reordering. You own the data: the table reports the move via `onRowOrderChange: (activeRowId, overRowId) => void`, and it is up to you to reorder your array. Provide `getRowId` so the ids passed to the callback are stable, and typically set `enablePagination: false` so dragging spans the full list. Example (`row-ordering`): ```tsx function RowOrderingExample() { const [data, setData] = React.useState(() => makeUsers(12)) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowOrdering: true, enablePagination: false, onRowOrderChange: (activeId, overId) => setData((prev) => { const from = prev.findIndex((r) => r.id === activeId) const to = prev.findIndex((r) => r.id === overId) if (from === -1 || to === -1) return prev const next = [...prev] const [moved] = next.splice(from, 1) if (moved) next.splice(to, 0, moved) return next }), }) return } ``` ```tsx const [data, setData] = useState(initialData) const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowOrdering: true, enablePagination: false, onRowOrderChange: (activeRowId, overRowId) => { setData((prev) => { const next = [...prev] const from = next.findIndex((row) => row.id === activeRowId) const to = next.findIndex((row) => row.id === overRowId) const [moved] = next.splice(from, 1) next.splice(to, 0, moved) return next }) }, }) return ``` ## With virtualization Row ordering works with `enableRowVirtualization`. Drag a row toward the top or bottom edge of the table and it scrolls; the dragged row stays rendered while it moves past the rendered window, so you can drop it anywhere in a long list. Example (`row-ordering-virtualized`): ```tsx function VirtualizedRowOrderingExample() { const [data, setData] = React.useState(() => makeUsers(1000)) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowOrdering: true, enableRowVirtualization: true, enablePagination: false, onRowOrderChange: (activeId, overId) => setData((prev) => { const from = prev.findIndex((r) => r.id === activeId) const to = prev.findIndex((r) => r.id === overId) if (from === -1 || to === -1) return prev const next = [...prev] const [moved] = next.splice(from, 1) if (moved) next.splice(to, 0, moved) return next }), }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowOrdering: true, enableRowVirtualization: true, enablePagination: false, onRowOrderChange, // as above }) ``` > **Custom range extractor:** A `rangeExtractor` passed in `rowVirtualizerOptions` still picks the rendered rows; the table adds the dragged row to its result. ## Related - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. - [Table instance](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/table-instance): useDataTable returns the standard TanStack table instance with three additions: the table's UI state in table.getState() (density, editing, filter modes…), methods to change it on table itself, and the resolved options and DOM refs on table.dataTable. - [Row selection](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/row-selection): Set enableRowSelection: true to add a leading checkbox column. --- # Row dragging Rows can be dragged out of a table — onto another table, or onto a drop zone of your own — with the pointer, touch or the keyboard. (To reorder rows inside one table, see [Row ordering](/docs/guides/row-ordering).) ## Between tables Wrap the tables in `DataTableDndProvider`, turn on `enableRowDragging` on the table rows leave from, and move the row in `onRowDragEnd`: ```tsx import { DataTable, DataTableDndProvider, useDataTable } from "@/components/ui/data-table" function Lists() { const [todo, setTodo] = React.useState(initialTodo) const [done, setDone] = React.useState(initialDone) const todoTable = useDataTable({ data: todo, columns, getRowId: (row) => row.id, enableRowDragging: true, onRowDragEnd: ({ row, over }) => { if (over.type === "table" && over.table === doneTable) { setTodo((rows) => rows.filter((r) => r.id !== row.id)) setDone((rows) => [...rows, row.original]) } }, }) const doneTable = useDataTable({ data: done, columns, getRowId: (row) => row.id }) return ( ) } ``` `over` says where the row landed: `{ type: "table", table, rowId? }` (the table, and the row it was dropped on, if any) or `{ type: "zone", id }`. While a row from another table is over a table, the table's root has a `data-drop-target` attribute, so you can highlight it with `data-[drop-target]:…` classes. A floating preview follows the pointer. Each table inside the provider keeps its own column and row reordering and drag-to-group, and the tables may share row and column ids. ## Drop zones Make any element a drop target with `useDataTableDropZone(id)` inside the provider: ```tsx import { useDataTableDropZone } from "@/components/ui/data-table" function Trash() { const { setNodeRef, isOver } = useDataTableDropZone("trash") return (
Drop here to delete
) } ``` A row dropped there calls `onRowDragEnd` with `over: { type: "zone", id: "trash" }`. Example (`row-dragging`): ```tsx function RowDraggingExample() { const [todo, setTodo] = React.useState([ { id: "t1", title: "Write the release notes" }, { id: "t2", title: "Review the migration guide" }, { id: "t3", title: "Record the demo" }, ]) const [done, setDone] = React.useState([ { id: "t4", title: "Ship slotProps" }, ]) const move = (from: "todo" | "done") => (task: Task, to: "other" | "trash") => { const [setFrom, setTo] = from === "todo" ? [setTodo, setDone] : [setDone, setTodo] setFrom((rows) => rows.filter((r) => r.id !== task.id)) if (to === "other") setTo((rows) => [...rows, task]) } return (
) } ``` ## Reordering too `enableRowDragging` only adds the drag handle; combine it with `enableRowOrdering` (and `onRowOrderChange`) to reorder inside the table as well. ## Relevant options | Name | Type | Default | Description | | --- | --- | --- | --- | | `enableRowDragging` | `boolean` | `false` | Adds a drag handle to each row for moving rows out of the table — to another table or a drop zone in the same `DataTableDndProvider` — without reordering inside it (combine with `enableRowOrdering` for both). Default false. | | `onRowDragEnd` | `(props: { row: DataTableRow table: DataTableInstance over: RowDropTarget }) => void` | | Called when a row is dropped on another table or a drop zone in the same `DataTableDndProvider`. Move the row in your data. | | `enableRowOrdering` | `boolean` | `false` | Drag-and-drop row reordering (adds a drag-handle column). | | `onRowOrderChange` | `(activeRowId: string, overRowId: string) => void` | | Called on a row drag-and-drop reorder; the consumer reorders its data. | ## Related - [Row ordering](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/row-ordering): Set enableRowOrdering: true to add a drag-handle column for drag-and-drop row reordering. - [Composition](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/composition): lays out the whole table for you. --- # Row pinning Set `enableRowPinning: true` to add a pin toggle inside the row-number column. Pinned rows stick to the top while you scroll the rest of the table. Because the toggle lives in the row-number column, row pinning pairs with `enableRowNumbers: true`. Example (`row-pinning`): ```tsx function RowPinningExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowNumbers: true, enableRowPinning: true, initialState: { pagination: { pageSize: 10 } }, }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowNumbers: true, enableRowPinning: true, }) return ``` > **Enable row numbers:** Turn on `enableRowNumbers` so the pin toggle has a column to live in. ## Display modes `rowPinningDisplayMode` sets where pinned rows show and how rows get pinned: | Mode | Pinned rows | Pinned by | | --- | --- | --- | | `"top-and-bottom"` (default) | Stay in view above (or below) the other rows while scrolling | The pin button (to the top), or `row.pin("bottom")` | | `"top"` / `"bottom"` | The same | The pin button, to that side | | `"static"` | Listed first (or last), and scroll away with the rest | The pin button | | `"select-top"` / `"select-bottom"` | Stay in view on that side | Selecting the row; deselecting unpins it. No pin buttons | ```tsx useDataTable({ data, columns, enableRowSelection: true, enableRowPinning: true, rowPinningDisplayMode: "select-top", // selected rows stay in view at the top }) ``` Sticky pinned rows have a solid line and a soft shadow on the edge that faces the other rows. They need a bounded scroll surface, which the table has by default (see [Sticky header & footer](/docs/guides/sticky)). Example (`row-pinning-modes`): ```tsx function RowPinningModesExample() { const [mode, setMode] = React.useState("top-and-bottom") const data = React.useMemo(() => makeUsers(60), []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enablePagination: false, enableRowPinning: true, enableRowSelection: true, rowPinningDisplayMode: mode, initialState: { rowPinning: { top: ["U-1004"], bottom: ["U-1050"] } }, slotProps: { surface: { className: "max-h-[420px]" } }, }) return (
) } ``` ## Related - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. - [Table instance](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/table-instance): useDataTable returns the standard TanStack table instance with three additions: the table's UI state in table.getState() (density, editing, filter modes…), methods to change it on table itself, and the resolved options and DOM refs on table.dataTable. - [Row numbers](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/row-numbers): Set enableRowNumbers: true to add a leading row-number column to the table. --- # Row numbers Set `enableRowNumbers: true` to add a leading row-number column to the table. Example (`row-numbers`): ```tsx function RowNumbersExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowNumbers: true, rowNumberMode: "static", initialState: { pagination: { pageSize: 10 } }, }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowNumbers: true, }) return ``` ## Numbering mode `rowNumberMode` controls how the numbers are computed: - `"static"` (default) numbers the current view and is page-aware, so the first visible row on each page restarts the count appropriately. - `"original"` uses the source row index, so each row keeps the same number regardless of sorting or pagination. ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowNumbers: true, rowNumberMode: "original", }) ``` > **Pairs with row pinning:** The row-number column is also where the pin toggle lives, so it works well alongside row pinning. ## Related - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. - [Table instance](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/table-instance): useDataTable returns the standard TanStack table instance with three additions: the table's UI state in table.getState() (density, editing, filter modes…), methods to change it on table itself, and the resolved options and DOM refs on table.dataTable. - [Row pinning](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/row-pinning): Set enableRowPinning: true to add a pin toggle inside the row-number column. --- # Sorting Sorting is on by default. Click a column header to cycle ascending → descending → unsorted; the header shows the current direction and the column-actions menu offers explicit sort/clear items. Example (`basic`): ```tsx function BasicExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { pagination: { pageSize: 10 } }, }) return } ``` ## Multi-column sorting Enable `enableMultiSort` to sort by several columns at once (shift-click headers). An order badge indicates each column's position in the sort. ```tsx const table = useDataTable({ data, columns, enableMultiSort: true, initialState: { sorting: [ { id: "department", desc: false }, { id: "salary", desc: true }, ], }, }) ``` Example (`sorting`): ```tsx function SortingExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableMultiSort: true, initialState: { sorting: [ { id: "department", desc: false }, { id: "salary", desc: true }, ], pagination: { pageSize: 10 }, }, }) return } ``` ## Per-column control Sorting is standard TanStack, so the usual column knobs apply: - `enableSorting: false` — disable sorting for a column. - `sortDescFirst: true` — start from descending (handy for numeric columns). - `sortFn` — a built-in sort function's name, a comparator, or the name of one of your own from the `sortFns` option. ```tsx const columns: DataTableColumnDef[] = [ { accessorKey: "id", header: "ID", enableSorting: false }, { accessorKey: "salary", header: "Salary", sortDescFirst: true }, ] ``` To reuse a comparator across columns, register it by name with `sortFns`: ```tsx const PRIORITY = ["low", "medium", "high"] const table = useDataTable({ data, columns: [{ accessorKey: "priority", header: "Priority", sortFn: "byPriority" }], sortFns: { byPriority: (rowA, rowB, columnId) => PRIORITY.indexOf(rowA.getValue(columnId)) - PRIORITY.indexOf(rowB.getValue(columnId)), }, }) ``` Example (`custom-sort-fns`): ```tsx function CustomSortFnsExample() { const data = React.useMemo(() => baseUsers, []) const columns = React.useMemo[]>( () => userColumns().map((column) => "accessorKey" in column && column.accessorKey === "role" ? { ...column, sortFn: "byRole" } : column ), [] ) const table = useDataTable({ data, columns, getRowId: (row) => row.id, // Sort roles by rank, not alphabetically. sortFns: { byRole: (rowA, rowB, columnId) => ROLE_ORDER.indexOf(rowA.getValue(columnId)) - ROLE_ORDER.indexOf(rowB.getValue(columnId)), }, initialState: { sorting: [{ id: "role", desc: false }], pagination: { pageSize: 10 }, }, }) return } ``` ## Controlled sorting Pass `state.sorting` and `onSortingChange` to control it yourself — for example to sort on the server (`manualSorting: true`). ```tsx const [sorting, setSorting] = React.useState([]) const table = useDataTable({ data, columns, state: { sorting }, onSortingChange: setSorting, manualSorting: true, }) ``` ## Related - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. - TanStack Table [sorting guide](https://tanstack.com/table/latest). --- # Grouping & aggregation Set `enableGrouping: true` to let users group rows by a column. Once enabled, users can drag a column header onto the drop-to-group zone, or pick **Group by** from a column's actions menu. Example (`grouping`): ```tsx function GroupingExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableGrouping: true, enableStickyFooter: true, initialState: { grouping: ["department"], expanded: true, pagination: { pageSize: 50 }, }, }) return } ``` ## Initial groups Seed the grouped state with `initialState.grouping` and expand all groups on first render with `initialState.expanded`. ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableGrouping: true, initialState: { grouping: ["department"], expanded: true, }, }) return ``` ## Aggregations Aggregations use the standard TanStack Table column options. Set `aggregationFn` to a built-in (`"sum"`, `"mean"`, ...), a custom function (built with TanStack v9's `constructAggregationFn({ aggregate })`), or the name of one registered with the `aggregationFns` option (see below), render the rolled-up value with `aggregatedCell`, and add a `footer` for a grand total. ```tsx const columns = [ { accessorKey: "salary", header: "Salary", aggregationFn: "sum", aggregatedCell: ({ getValue }) => ( {formatCurrency(getValue())} ), footer: ({ table }) => { const total = table .getRowModel() .rows.reduce((sum, row) => sum + row.getValue("salary"), 0) return {formatCurrency(total)} }, }, ] ``` To reuse an aggregation across columns, register it by name with `aggregationFns`. Each entry has an `aggregate` function that receives the group's `rows` and a `getValue(row)` helper: ```tsx const table = useDataTable({ data, columns: [ { accessorKey: "department", header: "Department" }, { accessorKey: "salary", header: "Salary", aggregationFn: "spread" }, ], aggregationFns: { spread: { aggregate: ({ rows, getValue }) => { const values = rows.map((row) => Number(getValue(row))) return Math.max(...values) - Math.min(...values) }, }, }, enableGrouping: true, }) ``` Pass several aggregations as an array and each result shows with its label — `Sum 4,000 · Mean 2,000`, with numbers in the table's language. Rename the labels with `localization.aggregationFns`, or render them yourself with `aggregatedCell`, which receives every result (`getValue()` returns `{ sum, mean }`). ```tsx { accessorKey: "salary", header: "Salary", aggregationFn: ["sum", "mean"], } ``` > **Keep totals in view:** Grouping pairs well with a pinned footer. Add `enableStickyFooter: true` so > the grand total stays visible while scrolling long groups. Example (`custom-aggregation`): ```tsx function CustomAggregationExample() { const data = React.useMemo(() => baseUsers, []) const columns = React.useMemo[]>( () => [ { accessorKey: "department", header: "Department" }, { accessorKey: "firstName", header: "First name" }, { accessorKey: "salary", header: "Salary", aggregationFn: "spread", cell: ({ getValue }) => currency.format(getValue() as number), aggregatedCell: ({ getValue }) => ( spread {currency.format(getValue() as number)} ), meta: { align: "right" }, }, ], [] ) const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableGrouping: true, // The gap between the highest and lowest salary in each group. aggregationFns: { spread: { aggregate: ({ rows, getValue }) => { const values = rows.map((row) => Number(getValue(row))) return Math.max(...values) - Math.min(...values) }, }, }, initialState: { grouping: ["department"], pagination: { pageSize: 20 } }, }) return } ``` Example (`multi-aggregation`): ```tsx function MultiAggregationExample() { const data = React.useMemo(() => baseUsers, []) const columns = React.useMemo[]>( () => [ { accessorKey: "department", header: "Department" }, { accessorKey: "firstName", header: "First name" }, { accessorKey: "salary", header: "Salary", aggregationFn: ["sum", "mean", "max"], cell: ({ getValue }) => currency.format(getValue() as number), meta: { align: "right" }, }, { accessorKey: "age", header: "Age", aggregationFn: ["min", "max"] }, ], [] ) const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableGrouping: true, initialState: { grouping: ["department"], pagination: { pageIndex: 0, pageSize: 20 }, }, }) return } ``` ## Grouped column mode TanStack's `groupedColumnMode` decides where a grouped column goes: `"reorder"` (default) moves it first, `"remove"` hides it, and `false` leaves it in place. Example (`grouped-column-mode`): ```tsx function GroupedColumnModeExample() { const [mode, setMode] = React.useState( "reorder" ) const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableGrouping: true, // TanStack's own option: move grouped columns first, hide them, or leave them. groupedColumnMode: mode, initialState: { grouping: ["department"], expanded: true, pagination: { pageIndex: 0, pageSize: 20 }, }, }) return (
) } ``` ## Custom group cells For finer control over how grouped rows render, the column `meta` exposes render hooks that receive the full table instance: `renderGroupedCell` (the group-header cell), `renderAggregatedCell` (overrides `aggregatedCell`), and `renderPlaceholderCell` (cells with no value because another column owns the group; empty by default). ```tsx { accessorKey: "department", header: "Department", meta: { renderGroupedCell: ({ row }) => ( {row.getValue("department")} · {row.subRows.length} ), }, } ``` Example (`customized-grouping`): ```tsx function CustomizedGroupingExample() { const data = React.useMemo(() => baseUsers, []) const columns = React.useMemo[]>( () => userColumns().map((column) => "accessorKey" in column && column.accessorKey === "department" ? { ...column, meta: { ...column.meta, // A custom group header: name, count and total salary. renderGroupedCell: ({ row }) => ( {String(row.getValue("department"))} {row.subRows.length} {currency.format( row.subRows.reduce( (sum, r) => sum + (r.original as User).salary, 0 ) )} ), }, } : column ), [] ) const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableGrouping: true, initialState: { grouping: ["department"], density: "compact", pagination: { pageIndex: 0, pageSize: 20 }, }, }) return } ``` ## Related - [Sticky header & footer](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/sticky): The header row (enableStickyHeader, default true) sticks to the top of the scroll surface. - [Column options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/column-options): Per-column configuration lives on the standard TanStack columnDef.meta object, which the table augments with the fields below (fully typed wherever meta is read). --- # Detail panel Provide `renderDetailPanel` to render an expandable panel beneath each row. An expand chevron column is added automatically, and row expansion is enabled for you. Example (`detail-panel`): ```tsx function DetailPanelExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { pagination: { pageSize: 10 } }, renderDetailPanel: ({ row }) => (
{row.original.city} {row.original.country} {row.original.startDate.toLocaleDateString()} {currency.format(row.original.salary)}
), }) return } ``` ## Usage `renderDetailPanel` receives `{ row, table }`. Return any React node — here a small definition list built from `row.original`. ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, renderDetailPanel: ({ row }) => (
Email
{row.original.email}
Phone
{row.original.phone}
Address
{row.original.address}
), }) return ``` > **Expansion is automatic:** You don't need to configure `getExpandedRowModel` or add a chevron column > yourself — defining `renderDetailPanel` turns on row expansion and inserts the > expander column. ## Charts and lazy content The panel renders only while the row is expanded, so it can hold anything — a chart, or content that loads when the row opens. Example (`chart-detail-panel`): ```tsx function ChartDetailPanelExample() { const data = React.useMemo(() => baseUsers.slice(0, 10), []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enablePagination: false, renderDetailPanel: ({ row }) => { // Deterministic monthly hours per person. const seed = Number(row.original.id.replace(/\D/g, "")) const hours = MONTHS.map((month, i) => ({ month, hours: 120 + ((seed * (i + 3)) % 60), })) return ( ) }, }) return } ``` Example (`lazy-detail-panel`): ```tsx function LazyDetailPanelExample() { const data = React.useMemo(() => baseUsers.slice(0, 10), []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enablePagination: false, // The panel mounts on expand, so it loads only then. renderDetailPanel: ({ row }) => , }) return } ``` ## Positioning the expander The expander column leads the data columns by default. Set `positionExpandColumn: "last"` to move it to the trailing edge. ```tsx useDataTable({ renderDetailPanel: ({ row }) => , positionExpandColumn: "last", // "first" | "last" }) ``` ## Related - [Tree data](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/tree): For hierarchical data, provide TanStack Table's getSubRows to tell the table where each row's children live. --- # Tree data For hierarchical data, provide TanStack Table's `getSubRows` to tell the table where each row's children live. Expansion turns on automatically. Example (`tree`): ```tsx function TreeExample() { const data = React.useMemo(() => orgData, []) const columns = React.useMemo(() => orgColumns, []) const table = useDataTable({ data, columns, getRowId: (row) => row.id, getSubRows: (row) => row.subRows, enablePagination: false, initialState: { expanded: true }, }) return } ``` ## Usage Return the children array from each row. Set `initialState.expanded: true` to start fully expanded, and usually disable pagination so the tree isn't split across pages. ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, getSubRows: (row) => row.subRows, enablePagination: false, initialState: { expanded: true, }, }) return ``` > **Indentation:** Use `row.depth` inside a cell renderer to indent child rows so the hierarchy reads clearly. ## From a flat list APIs often return trees as a flat list with parent ids. Build `subRows` once with `useMemo` and pass `getSubRows` as usual. Example (`tree-from-flat`): ```tsx function TreeFromFlatExample() { // Build `subRows` from `parentId` once. const data = React.useMemo(() => { const byId = new Map( FLAT_ORG.map((node) => [node.id, { ...node }]) ) const roots: FlatNode[] = [] for (const node of byId.values()) { const parent = node.parentId ? byId.get(node.parentId) : undefined if (parent) (parent.subRows ??= []).push(node) else roots.push(node) } return roots }, []) const table = useDataTable({ data, columns: [ { accessorKey: "name", header: "Name" }, { accessorKey: "title", header: "Title" }, ], getRowId: (row) => row.id, getSubRows: (row) => row.subRows, enablePagination: false, initialState: { expanded: true }, }) return } ``` ## Loading children on expand Let rows expand before their children exist with TanStack's `getRowCanExpand`, and load the children when the row expands (control `expanded` and watch it in `onExpandedChange`). Example (`lazy-sub-rows`): ```tsx function LazySubRowsExample() { const [data, setData] = React.useState(ROOT_FOLDERS) const [expanded, setExpanded] = React.useState({}) const loading = React.useRef(new Set()) const table = useDataTable({ data, columns: [{ accessorKey: "name", header: "Folder" }], getRowId: (row) => row.id, getSubRows: (row) => row.subRows, // Rows can expand before their children are loaded. getRowCanExpand: (row) => row.original.hasChildren, enablePagination: false, state: { expanded }, onExpandedChange: (updater) => { const next = typeof updater === "function" ? updater(expanded) : updater setExpanded(next) // Load the children of each newly expanded row that has none yet. for (const id of Object.keys(next === true ? {} : next)) { const row = table.getRow(id) if (row.original.subRows || loading.current.has(id)) continue loading.current.add(id) void delay(500).then(() => { setData((prev) => withChildren( prev, id, [1, 2, 3].map((n) => ({ id: `${id}-${n}`, name: `${row.original.name} ${n}`, hasChildren: id.split("-").length < 2, })) ) ) }) } }, }) return } ``` ## Editing tree rows Editing works at any depth: `onEditingRowSave` gets the row, and you update the matching node in your nested data. Example (`editing-tree`): ```tsx function EditingTreeExample() { const [data, setData] = React.useState(orgData) const table = useDataTable({ data, columns: [ { accessorKey: "name", header: "Name" }, { accessorKey: "title", header: "Title" }, { accessorKey: "headcount", header: "Headcount", meta: { editVariant: "number", align: "right" }, }, ], getRowId: (row) => row.id, getSubRows: (row) => row.subRows, enablePagination: false, enableEditing: true, editDisplayMode: "row", initialState: { expanded: true }, onEditingRowSave: ({ row, values, exit }) => { setData((prev) => updateNode(prev, row.id, values as Partial)) exit() }, }) return } ``` ## Expand-all toggle The expand column's header toggles every row. Hide it with `enableExpandAll: false`. ## Related - [Detail panel](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/detail-panel): Provide renderDetailPanel to render an expandable panel beneath each row. - [Grouping & aggregation](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/grouping): Set enableGrouping: true to let users group rows by a column. --- # Editing Set `enableEditing: true` to make rows editable, then choose how editing is presented with `editDisplayMode`. Per-column behavior is configured through column `meta`. ## Cell editing `editDisplayMode: "cell"` is the default. Click a cell to edit it inline and commit the new value with `onEditingCellSave`. Example (`editing-cell`): ```tsx // function EditingExample({ mode }: { mode: EditDisplayMode }) { const [data, setData] = React.useState(() => makeUsers(12)) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableEditing: true, editDisplayMode: mode, enablePagination: false, createRowDefaults: { firstName: "", lastName: "", email: "", role: "Viewer", department: "Engineering", status: "pending", age: 30, salary: 50000, }, onEditingCellSave: ({ row, column, value }) => setData((prev) => prev.map((r) => r.id === row.id ? ({ ...r, [column.id]: value } as User) : r ) ), onEditingRowSave: ({ row, values, exit }) => { setData((prev) => prev.map((r) => r.id === row.id ? { ...r, ...(values as Partial) } : r ) ) exit() }, onCreatingRowSave: ({ values, exit }) => { setData((prev) => [ { ...makeUsers(1)[0]!, id: `U-${2000 + prev.length}`, ...(values as Partial), }, ...prev, ]) exit() }, renderTopToolbarCustomActions: ({ table }) => ( ), }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableEditing: true, editDisplayMode: "cell", onEditingCellSave: ({ row, column, value }) => { setData((prev) => prev.map((r) => (r.id === row.id ? { ...r, [column.id]: value } : r)) ) }, }) return ``` ## Row editing `editDisplayMode: "row"` edits a whole row at once with save/cancel controls. Persist the row with `onEditingRowSave`, then call `exit()` to leave edit mode. Example (`editing-row`): ```tsx // function EditingExample({ mode }: { mode: EditDisplayMode }) { const [data, setData] = React.useState(() => makeUsers(12)) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableEditing: true, editDisplayMode: mode, enablePagination: false, createRowDefaults: { firstName: "", lastName: "", email: "", role: "Viewer", department: "Engineering", status: "pending", age: 30, salary: 50000, }, onEditingCellSave: ({ row, column, value }) => setData((prev) => prev.map((r) => r.id === row.id ? ({ ...r, [column.id]: value } as User) : r ) ), onEditingRowSave: ({ row, values, exit }) => { setData((prev) => prev.map((r) => r.id === row.id ? { ...r, ...(values as Partial) } : r ) ) exit() }, onCreatingRowSave: ({ values, exit }) => { setData((prev) => [ { ...makeUsers(1)[0]!, id: `U-${2000 + prev.length}`, ...(values as Partial), }, ...prev, ]) exit() }, renderTopToolbarCustomActions: ({ table }) => ( ), }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableEditing: true, editDisplayMode: "row", onEditingRowSave: ({ row, values, exit }) => { setData((prev) => prev.map((r) => (r.id === row.id ? { ...r, ...values } : r)) ) exit() }, }) return ``` ## Table editing `editDisplayMode: "table"` keeps every editable cell in edit mode at all times, like a spreadsheet. Example (`editing-table`): ```tsx // function EditingExample({ mode }: { mode: EditDisplayMode }) { const [data, setData] = React.useState(() => makeUsers(12)) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableEditing: true, editDisplayMode: mode, enablePagination: false, createRowDefaults: { firstName: "", lastName: "", email: "", role: "Viewer", department: "Engineering", status: "pending", age: 30, salary: 50000, }, onEditingCellSave: ({ row, column, value }) => setData((prev) => prev.map((r) => r.id === row.id ? ({ ...r, [column.id]: value } as User) : r ) ), onEditingRowSave: ({ row, values, exit }) => { setData((prev) => prev.map((r) => r.id === row.id ? { ...r, ...(values as Partial) } : r ) ) exit() }, onCreatingRowSave: ({ values, exit }) => { setData((prev) => [ { ...makeUsers(1)[0]!, id: `U-${2000 + prev.length}`, ...(values as Partial), }, ...prev, ]) exit() }, renderTopToolbarCustomActions: ({ table }) => ( ), }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableEditing: true, editDisplayMode: "table", onEditingCellSave: ({ row, column, value }) => { setData((prev) => prev.map((r) => (r.id === row.id ? { ...r, [column.id]: value } : r)) ) }, }) ``` ## Modal editing & creating `editDisplayMode: "modal"` edits (and creates) rows in a dialog. Use `onEditingRowSave` for edits and `onCreatingRowSave` for new rows; both receive `exit()` to close the dialog. Seed the create form with `createRowDefaults`, and open it from the toolbar via `table.beginCreate()`. Example (`editing-modal`): ```tsx // function EditingExample({ mode }: { mode: EditDisplayMode }) { const [data, setData] = React.useState(() => makeUsers(12)) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableEditing: true, editDisplayMode: mode, enablePagination: false, createRowDefaults: { firstName: "", lastName: "", email: "", role: "Viewer", department: "Engineering", status: "pending", age: 30, salary: 50000, }, onEditingCellSave: ({ row, column, value }) => setData((prev) => prev.map((r) => r.id === row.id ? ({ ...r, [column.id]: value } as User) : r ) ), onEditingRowSave: ({ row, values, exit }) => { setData((prev) => prev.map((r) => r.id === row.id ? { ...r, ...(values as Partial) } : r ) ) exit() }, onCreatingRowSave: ({ values, exit }) => { setData((prev) => [ { ...makeUsers(1)[0]!, id: `U-${2000 + prev.length}`, ...(values as Partial), }, ...prev, ]) exit() }, renderTopToolbarCustomActions: ({ table }) => ( ), }) return } ``` ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableEditing: true, editDisplayMode: "modal", createRowDefaults: { name: "", role: "member" }, onEditingRowSave: ({ row, values, exit }) => { setData((prev) => prev.map((r) => (r.id === row.id ? { ...r, ...values } : r)) ) exit() }, onCreatingRowSave: ({ values, exit }) => { setData((prev) => [...prev, { id: crypto.randomUUID(), ...values }]) exit() }, renderTopToolbarCustomActions: ({ table }) => ( ), }) ``` ### Cancelling `onEditingRowCancel({ row, table })` and `onCreatingRowCancel({ table })` run when the user cancels — the Cancel button, closing the dialog, or the row's cancel button. They don't run after a save calls `exit()`. ### Custom dialog content `renderEditRowDialogContent` and `renderCreateRowDialogContent` replace what's inside the dialog. They receive the built-in fields as `internalEditComponents`, plus `save()`, `cancel()` and `hasErrors`. Include a `DialogTitle` so the dialog keeps its accessible name. ```tsx import { DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" useDataTable({ data, columns, enableEditing: true, editDisplayMode: "modal", renderEditRowDialogContent: ({ row, internalEditComponents, save, cancel, hasErrors }) => ( <> Edit {row.original.name}
{internalEditComponents}
), }) ``` Example (`edit-dialog`): ```tsx function EditDialogExample() { const [data, setData] = React.useState(() => baseUsers.slice(0, 12)) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableEditing: true, editDisplayMode: "modal", onEditingRowSave: ({ row, values, exit }) => { setData((prev) => prev.map((user) => (user.id === row.id ? { ...user, ...values } : user)) ) exit() }, // Two columns of fields and our own buttons. renderEditRowDialogContent: ({ row, internalEditComponents, save, cancel, hasErrors, }) => ( <> Edit {row.original.firstName} {row.original.lastName}
{internalEditComponents}
), }) return } ``` ### Inline create row position With `createDisplayMode: "row"`, the create row sits above the data. Set `positionCreatingRow: "bottom"` to put it below, or a row index (`positionCreatingRow: 3`) to insert it before that row on the current page. Example (`create-row`): ```tsx function CreateRowExample() { const [data, setData] = React.useState(() => baseUsers.slice(0, 8)) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableEditing: true, createDisplayMode: "row", positionCreatingRow: "bottom", createRowDefaults: { role: "Viewer", status: "pending" }, onCreatingRowSave: ({ values, exit }) => { setData((prev) => [ ...prev, { ...baseUsers[0]!, ...values, id: `U-new-${prev.length}` } as User, ]) exit() }, renderTopToolbarCustomActions: ({ table }) => ( ), }) return } ``` ## Per-column editing config Configure editors on each column's `meta`: - `editVariant: "text" | "number" | "select"` — the input type. - `editSelectOptions` — options for the `"select"` variant, or `({ row, column, table }) => options` to vary them by row (`row` is undefined when creating). - `validate: (value) => string | undefined` — return a message to block the save. - `enableEditing: false` — lock a single column, or `(row) => boolean` to lock it on some rows. ```tsx const columns = [ { accessorKey: "role", header: "Role", meta: { editVariant: "select", editSelectOptions: ["admin", "member", "viewer"], }, }, { accessorKey: "email", header: "Email", meta: { editVariant: "text", validate: (value) => /.+@.+\..+/.test(value) ? undefined : "Enter a valid email", }, }, { accessorKey: "id", header: "ID", meta: { enableEditing: false }, }, ] ``` ## Editing some rows only Pass a function as `enableEditing` to decide row by row. Rows it returns `false` for show no click-to-edit cells and no edit button; creating new rows still works. The same works per column with `meta.enableEditing`. ```tsx const table = useDataTable({ data, columns: [ { accessorKey: "name", header: "Name" }, { accessorKey: "salary", header: "Salary", // Only managers' salaries are editable. meta: { enableEditing: (row) => row.original.role === "manager" }, }, ], enableEditing: (row) => !row.original.archived, }) ``` To check a row yourself, call `table.dataTable.getCanEditRow(row)`. Example (`editing-per-row`): ```tsx function EditingPerRowExample() { const [data, setData] = React.useState(() => baseUsers.slice(0, 12)) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableEditing: (row) => row.original.status !== "inactive", editDisplayMode: "cell", onEditingCellSave: ({ row, column, value }) => setData((prev) => prev.map((user) => user.id === row.id ? { ...user, [column.id]: value } : user ) ), slotProps: { bodyRow: ({ row }) => ({ className: row.original.status === "inactive" ? "text-muted-foreground" : undefined, }), }, }) return } ``` ## Custom editor When `editVariant` isn't enough, supply your own editor with `meta.renderEditCell`. It replaces the built-in field while the cell/row is editing; drive the value through the table instance (`setRowDraftValue` for row/modal editing, or `onEditingCellSave` for cell editing): ```tsx { accessorKey: "status", header: "Status", meta: { renderEditCell: ({ cell, table }) => ( table.setRowDraftValue(cell.column.id, value) } /> ), }, } ``` ## Relevant options `useDataTable` options that drive editing and creating: | Name | Type | Default | Description | | --- | --- | --- | --- | | `enableEditing` | `boolean \| ((row: DataTableRow) => boolean)` | `false` | Enable inline editing, or decide per row with a function (rows it returns false for can't be edited; creating rows is still allowed). | | `editDisplayMode` | `EditDisplayMode` | `"cell"` | How edits are surfaced. Default "cell". | | `createDisplayMode` | `CreateDisplayMode` | `"modal"` | How the create form is surfaced, independent of `editDisplayMode`: `"modal"` (dialog, default), `"row"` (an inline editable row at the top of the table), or `"custom"` (you render it from `isCreating` + `rowDraft`). | | `createRowDefaults` | `Record` | | Default values for the create form, keyed by column id. | | `onEditingCellSave` | `(props: { row: DataTableRow column: DataTableColumn value: unknown table: DataTableInstance }) => void` | | Called when a cell edit is committed (cell and table editing) with the new `value`. Persist it; the table does not change `data` itself. | | `onEditingRowSave` | `(props: { row: DataTableRow values: Record table: DataTableInstance exit: () => void }) => void` | | Called when a row edit is saved (row and modal editing) with the edited `values`. Call `exit()` to leave editing, e.g. after the save succeeds. | | `onCreatingRowSave` | `(props: { values: Record table: DataTableInstance exit: () => void }) => void` | | Called when the new-row editor is saved with the row's `values`. Call `exit()` to close it. | Per-column, via `columnDef.meta`: | meta key | Type | Default | Description | | --- | --- | --- | --- | | `enableEditing` | `boolean \| ((row: DataTableRow) => boolean)` | | Allow editing this column (defaults to true when table editing is on), or decide per row with a function. | | `editVariant` | `EditVariant` | | Inline editor variant. Defaults to "text". | | `editSelectOptions` | `\| DataTableFilterOption[] \| ((props: { row?: DataTableRow column: DataTableColumn table: DataTableInstance }) => DataTableFilterOption[])` | | Options for the "select" edit variant, or a function returning them for the row being edited (`row` is undefined in the create form). | | `renderEditCell` | `(props: CellRenderProps) => React.ReactNode` | | Custom inline editor for this column (escape hatch). Replaces the built-in editor while the cell/row is editing; read the draft from `table.getState().rowDraft` and write it with `table.setRowDraftValue` (row editing), or save with `onEditingCellSave` (cell editing). | | `validate` | `(value: unknown) => string \| undefined` | | Validate an edited value; return an error message or undefined if valid. | ## Related - [Display modes](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/display-modes): Several features expose a *DisplayMode option that changes _how_ the UI is presented without changing the underlying behavior. - [Column options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/column-options): Per-column configuration lives on the standard TanStack columnDef.meta object, which the table augments with the fields below (fully typed wherever meta is read). - [Table instance](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/table-instance): useDataTable returns the standard TanStack table instance with three additions: the table's UI state in table.getState() (density, editing, filter modes…), methods to change it on table itself, and the resolved options and DOM refs on table.dataTable. - [Row actions](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/row-actions): Provide renderRowActions to add a trailing actions column. --- # Row actions Provide `renderRowActions` to add a trailing actions column. It receives `{ row, table }` and returns whatever controls you need — for example a delete button. Example (`row-actions`): ```tsx function RowActionsExample() { const [data, setData] = React.useState(() => baseUsers) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { pagination: { pageSize: 10 } }, renderRowActions: ({ row }) => ( ), }) return } ``` ## Usage ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, renderRowActions: ({ row }) => ( ), }) return ``` > **Combine with editing:** Row actions sit alongside editing controls — wire up an edit button here to > start row editing programmatically. ## As a menu For a compact kebab menu instead of inline buttons, use `renderRowActionMenuItems`. It returns the menu items (e.g. `DropdownMenuItem`s) rendered inside an auto-injected dropdown in the actions column. Both slots can be combined — inline controls plus a menu. While editing is on (`"row"` or `"modal"` display), the Edit action moves into this menu: it arrives as `internalMenuItems`. The callback also gets `closeMenu()`. ```tsx import { DropdownMenuItem } from "@/components/ui/dropdown-menu" const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableEditing: true, editDisplayMode: "modal", renderRowActionMenuItems: ({ row, internalMenuItems }) => ( <> {internalMenuItems} remove(row.id)}> Delete ), }) ``` Example (`row-actions-menu`): ```tsx function RowActionsMenuExample() { const [data, setData] = React.useState(() => baseUsers.slice(0, 12)) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableEditing: true, editDisplayMode: "modal", onEditingRowSave: ({ row, values, exit }) => { setData((prev) => prev.map((user) => (user.id === row.id ? { ...user, ...values } : user)) ) exit() }, // The built-in Edit item, then our own. renderRowActionMenuItems: ({ row, internalMenuItems }) => ( <> {internalMenuItems} setData((prev) => prev.filter((user) => user.id !== row.id)) } > Delete ), }) return } ``` ## Positioning the column The actions column trails the data columns by default. Set `positionActionsColumn: "first"` to move it to the leading edge. ```tsx useDataTable({ renderRowActions: ({ row }) => , positionActionsColumn: "first", // "first" | "last" }) ``` ## Related - [Cell actions](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/cell-actions): Cells can have a right-click menu. - [Editing](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/editing): Set enableEditing: true to make rows editable, then choose how editing is presented with editDisplayMode. --- # Cell actions Cells can have a right-click menu. Turn on the built-in one with `enableCellActions: true` — it offers **Copy** (when click-to-copy is on) and **Edit** (when the cell is editable) — or decide per cell with `enableCellActions: (cell) => boolean`. Example (`cell-actions`): ```tsx function CellActionsExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, // Copy from the right-click menu; it also gets the built-in Edit item. enableClickToCopy: "context-menu", enableCellActions: true, enableEditing: true, initialState: { pagination: { pageSize: 10 } }, renderCellActionMenuItems: ({ cell, internalMenuItems }) => ( <> {internalMenuItems} navigator.clipboard?.writeText(JSON.stringify(cell.row.original)) } > Copy row as JSON ), }) return } ``` ## Your own items `renderCellActionMenuItems` sets the menu's contents (and turns the menu on). It receives `{ cell, row, table, internalMenuItems, closeMenu }`: place `internalMenuItems` where the built-in items should go, or leave it out to replace them, and call `closeMenu()` from anything that isn't a menu item. ```tsx import { ContextMenuItem, ContextMenuSeparator } from "@/components/ui/context-menu" const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableCellActions: true, enableEditing: true, renderCellActionMenuItems: ({ cell, internalMenuItems }) => ( <> {internalMenuItems} search(String(cell.getValue()))}> Search for this ), }) return ``` ## Click to copy Enable click-to-copy on every cell with `enableClickToCopy: true`, or scope it to a single column through its `meta`. Set it to `"context-menu"` to copy from the cell menu instead of on click, or pass `(cell) => boolean | "context-menu"` to decide per cell. ```tsx const columns = [ { accessorKey: "email", header: "Email", meta: { enableClickToCopy: true }, }, ] ``` ## Column actions menu `renderColumnActionsMenuItems` sets the contents of every column header's actions menu. It receives `{ column, table, internalMenuItems, closeMenu }`: put `internalMenuItems` where the built-in sort / filter / hide / group / pin items should go, or leave it out to replace them. ```tsx import { DropdownMenuItem, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu" const table = useDataTable({ data, columns, getRowId: (row) => row.id, renderColumnActionsMenuItems: ({ column, internalMenuItems }) => ( <> {internalMenuItems} exportColumn(column.id)}> Export column ), }) ``` Example (`column-actions-menu`): ```tsx function ColumnActionsMenuExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { pagination: { pageSize: 10 } }, renderColumnActionsMenuItems: ({ column, table, internalMenuItems }) => ( <> {internalMenuItems} table.setColumnVisibility( Object.fromEntries( table .getAllLeafColumns() .map((c) => [c.id, c.id === column.id || !c.getCanHide()]) ) ) } > Show only this column ), }) return } ``` ## Related - [Row actions](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/row-actions): Provide renderRowActions to add a trailing actions column. --- # Event listeners The data table exposes click handlers for rows and cells so you can react to user interaction. Example (`event-listeners`): ```tsx function EventListenersExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const [last, setLast] = React.useState(null) const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { pagination: { pageSize: 10 } }, onRowClick: ({ row }) => setLast(row.original.email), renderTopToolbarCustomActions: () => ( {last ? `Last clicked: ${last}` : "Click any row"} ), }) return } ``` Row handlers (`onRowClick`, `onRowDoubleClick`) receive `{ row, table, event }`. Cell handlers (`onCellClick`, `onCellDoubleClick`) receive `{ cell, row, table, event }`. ```tsx import { useState } from "react" import { DataTable, useDataTable } from "@/components/ui/data-table" const [lastClickedId, setLastClickedId] = useState(null) const table = useDataTable({ data, columns, getRowId: (row) => row.id, onRowClick: ({ row }) => { setLastClickedId(row.id) }, }) return ``` > **Access the full context:** Because each handler receives the `table` instance and the original `event`, > you can read selection state, call other table APIs, or stop propagation > directly inside your handler. ## Related - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. --- # Density Density controls the vertical padding of table rows and the padding around the toolbars. The data table ships three densities: `"comfortable"` (the default), `"compact"`, and `"spacious"`. A toolbar button cycles through them, so end users can pick the row height that suits them. Example (`density`): ```tsx function DensityExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { density: "compact", pagination: { pageSize: 10 } }, }) return } ``` ## Usage Set the initial density with `initialState.density`: ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { density: "compact" }, }) return ``` ## Reading and setting density You can read or change the current density at any time through the table instance: ```tsx // current density: "comfortable" | "compact" | "spacious" const density = table.getState().density // switch to compact programmatically table.setDensity("compact") ``` This is handy when you want to drive density from your own controls instead of the built-in toolbar button. To show the current density in your component's render, add `stateSelector: (state) => state.density` (see [State management](/docs/guides/state#reading-state-in-your-component)). ## Hiding the toolbar button Each toolbar control can be hidden individually. Pass `enableDensityToggle: false` to drop the density button (for example when you drive density from your own UI), or `enableFullscreenToggle: false` to drop the full-screen button: ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableDensityToggle: false, enableFullscreenToggle: false, }) ``` The other toolbar options have their own flags too: `enableGlobalFilter`, `enableColumnFilters`, and `enableColumnActions`. To remove the entire top toolbar, use `enableTopToolbar: false`. > **Persisting density:** `density` is table state, so you can mirror it into local storage or a query > param (with `onDensityChange`) and restore it via `initialState.density` on > the next load, or control it with `state.density`. ## Related - [Table instance](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/table-instance): useDataTable returns the standard TanStack table instance with three additions: the table's UI state in table.getState() (density, editing, filter modes…), methods to change it on table itself, and the resolved options and DOM refs on table.dataTable. --- # Animations The table animates rows, columns, detail panels, the toolbar and a few controls so changes are easy to follow. It is on by default and turns itself off for people who prefer reduced motion. Example (`animations`): ```tsx function AnimationsExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const [animated, setAnimated] = React.useState(true) const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowSelection: true, enableAnimations: animated, initialState: { pagination: { pageIndex: 0, pageSize: 8 } }, renderDetailPanel: ({ row }) => (

{row.original.firstName} {row.original.lastName} is {row.original.role}{" "} in {row.original.department}, based in {row.original.city}.

), renderTopToolbarCustomActions: () => ( ), }) return } ``` ## What animates - **Row order:** rows glide to their new position when you sort, filter, pin, group or expand. - **New and removed rows:** new rows fade in with a small rise. Removed rows fade out. With [virtualization](/docs/guides/virtualization), rows that a sort or filter brings into the drawn window fade in too. - **Filter row and density:** rows glide when the filter row opens or closes and when the density changes, so the new spacing does not jump. - **Columns:** hiding, showing, pinning or reordering a column glides the columns that move. A shown column fades in, and a hidden column's header fades out where it was. - **Search box:** the box slides out beside the search button and back into it, pushing the toolbar buttons aside as it goes. - **Selection banner and grouping drop zone:** the "N selected" banner and the drop zone slide and fade in and out, and the table below glides to make room. - **Detail panels:** a panel opens from the top down while the rows below glide. It closes from the bottom up. - **Sort arrow:** the sort icon fades and scales between states. The default arrow also turns around for descending order, and when the sort is cleared it fades out without turning back. A custom sort icon never turns. - **Selected count:** the old "N selected" text fades out as the new one fades in, in the same place. A row that glides past others is drawn above them, and the row that moves farthest is on top. Motion pauses while a row is being dragged, while a cell or row is being edited and while a new row is being created. Starting an edit stops any row or column motion still running. ## Turning it off Set `enableAnimations: false`. ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableAnimations: false, }) return ``` You do not need to handle reduced motion yourself. When the user's system asks for reduced motion, the table does not animate, whatever `enableAnimations` is. The table root carries a `data-animations` attribute only while animations are active. ## Timing Three CSS variables set the timing. The table has built-in fallbacks, so you only set the ones you want to change. | Variable | Default | Used for | | --------------------------- | -------------------------------- | ------------------------------------------------- | | `--dt-motion-duration` | `280ms` | Row and column glide, search box, toolbar, detail panels, sort arrow, checkbox | | `--dt-motion-duration-fast` | `160ms` | Rows and columns fading in and out, banner, selected count | | `--dt-motion-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | Every animation | Set them with a class on the table. `className` goes on the table's root. ```tsx ``` Or in a stylesheet, for every table: ```css [data-slot="data-table"] { --dt-motion-duration: 400ms; --dt-motion-ease: ease-out; } ``` Each table times its own controls. A table with animations off, nested in an animated table's detail panel, keeps its controls still. ## Performance - Only rows that are drawn animate. With [virtualization](/docs/guides/virtualization), rows outside the window are not touched. - Scrolling never animates. - A change that would animate more than 300 rows just updates, with no motion. Above 100 gliding rows, row separators are hidden for the length of the glide. - Columns follow the same rule: only drawn columns glide. A change that would animate more than 3,000 cells (the table's rows times the columns that move or enter) just updates, with no motion. - Removed rows never make the table scroll further than its content. In a scrolled table, a removed row that sat below the table's new bottom disappears at once instead of fading, so the scroll position does not jump when the fade ends. ## Limits - Group header widths snap. A group header glides to its new position, but its width changes at once, for example when you hide one of its columns. - With [virtualization](/docs/guides/virtualization), a window change in the middle of a glide can shift the cells that are still gliding. A density change on a virtualized table that is scrolled down from the top just updates: the table measures the new rows and moves the scroll, so a glide would jump. - An edit that starts in the middle of a toolbar motion lets that toolbar motion finish. Row and column motion stops at once. - Toolbar groups are `position: relative`, so absolutely positioned content in a custom toolbar is placed against its group, not the toolbar. ## Related - [Sorting](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/sorting): Sorting is on by default. - [Virtualization](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/virtualization): Virtualization renders only the cells currently in view, keeping large tables fast. - [Detail panel](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/detail-panel): Provide renderDetailPanel to render an expandable panel beneath each row. --- # Sticky header & footer The header row (`enableStickyHeader`, default `true`) sticks to the top of the scroll surface. Enable `enableStickyFooter` (default `false`) to also pin the aggregation/footer row to the bottom. As you scroll through rows, both stay pinned in view. Example (`sticky`): ```tsx function StickyExample() { const data = React.useMemo(() => makeUsers(40), []) const columns = useUserColumns() const table = useDataTable({ slotProps: { surface: { className: "max-h-[460px]" } }, data, columns, getRowId: (row) => row.id, enablePagination: false, enableStickyFooter: true, }) return } ``` ## Bounded surface height Sticky behavior requires a **bounded** scroll surface — there is nothing to stick to if the table grows to fit all its rows. While `enableStickyHeader` is on (the default), the surface is automatically capped near the viewport height (`clamp(350px, 100dvh - 200px, 9999px)`), so tall tables scroll internally out of the box. The `- 200px` is a heuristic that reserves room for the toolbar and pagination — if the table sits below a lot of other page content, the surface can still extend past the viewport; pick your own bound in that case. To do so, set a max height with `slotProps.surface` — it overrides the default: ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableStickyHeader: true, enableStickyFooter: true, slotProps: { surface: { className: "max-h-[460px]" } }, }) return ``` Once the content overflows the bound, the header pins to the top and the footer pins to the bottom while the rows scroll between them. Setting `enableStickyHeader: false` removes both the pinning and the default bound, letting the table grow to its full height. > **MRT parity note:** Material React Table defaults `enableStickyHeader` and `enableStickyFooter` to `false`. This table deliberately defaults `enableStickyHeader` to `true` so headers stay useful out of the box, but leaves `enableStickyFooter` at `false` (opt in when you have an aggregation/footer row). The sticky-implies-bounded-surface behavior itself matches MRT. ## Footer cells Footer cells are populated from each column's `footer` definition or from aggregation values. See [Grouping](/docs/guides/grouping) for how aggregation rows are produced. > **Nothing to stick to?:** If the header or footer is not sticking, the surface is probably not bounded — check that `slotProps.surface` isn't removing the default max-height (e.g. `max-h-none`) and that the data actually overflows the bound. ## Related - [Grouping](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/grouping): Set enableGrouping: true to let users group rows by a column. - [DataTable props](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/data-table-props): is mostly driven by the instance from useDataTable. --- # Toolbar customization The table renders a **top toolbar** (title slot + your actions on the left, an icon-action cluster on the right) and a **bottom toolbar** (pagination). Every part can be hidden, replaced, or extended. ## Showing / hiding the toolbars ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableTopToolbar: false, // remove the entire top toolbar enableBottomToolbar: false, // remove the entire bottom toolbar }) ``` Example (`body-only`): ```tsx function BodyOnlyExample() { const table = useDataTable({ data: ACTIVITY, columns: [ { accessorKey: "who", header: "Who" }, { accessorKey: "what", header: "What" }, { accessorKey: "when", header: "When", meta: { align: "right" }, }, ], getRowId: (row) => row.id, enableTableHead: false, enableTopToolbar: false, enableBottomToolbar: false, enableKeyboardNavigation: false, initialState: { density: "compact" }, }) return } ``` ## Internal actions (the icon cluster) The right-hand cluster holds global search, the filter toggle, column visibility, the density toggle, and the full-screen toggle. Hide individual items, or the whole cluster: ```tsx useDataTable({ // per-item enableGlobalFilter: false, enableColumnFilters: false, enableColumnActions: false, // column visibility menu enableDensityToggle: false, enableFullscreenToggle: false, // …or hide all of them at once enableToolbarInternalActions: false, }) ``` ## Custom actions Add your own buttons without replacing anything. `renderTopToolbarCustomActions` fills the top toolbar's left region (next to the title); `renderBottomToolbarCustomActions` fills the bottom toolbar's left region (next to pagination): ```tsx useDataTable({ renderTopToolbarCustomActions: ({ table }) => ( ), renderBottomToolbarCustomActions: ({ table }) => ( {table.getSelectedRowModel().rows.length} selected ), }) ``` ## Facet filters `DataTableFacetFilter` puts a column filter on the toolbar as a button. It is dashed while nothing is picked and names the pick in a badge once something is. It reads and writes the table's `columnFilters` by id, so it works the same with or without the header filter row. `DataTableResetFilters` clears every column filter, and renders nothing while none is set. ```tsx useDataTable({ renderTopToolbarCustomActions: ({ table }) => ( <> {/* one choice; options come from the column's meta.filterSelectOptions */} {/* several choices, with a heading and a count beside each option */} ), }) ``` Example (`facet-filter`): ```tsx function FacetFilterExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() // A count beside each status, from the full data set. const statusCounts = React.useMemo(() => { const counts: Record = {} for (const user of data) counts[user.status] = (counts[user.status] ?? 0) + 1 return counts }, [data]) const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { pagination: { pageSize: 10 } }, renderTopToolbarCustomActions: ({ table }) => (
), }) return } ``` Pass `options` to list values the column doesn't define, and `title` to rename the button. For a filter your page keeps outside the table, pass `value` and `onValueChange`; the table's own `columnFilters` are then left alone. Give `DataTableResetFilters` the matching `active` and `onReset` so it clears that filter too. ## Replacing a whole toolbar For full control, replace the built-in toolbar entirely. The render slot receives the table instance: ```tsx useDataTable({ renderTopToolbar: ({ table }) => , renderBottomToolbar: ({ table }) => , // or replace just the icon cluster, keeping the title/actions layout: renderToolbarInternalActions: ({ table }) => , }) ``` ## Pagination placement `positionPagination` controls where the pagination controls render. `"none"` keeps pagination active (the data is still paged) but hides the controls. ```tsx useDataTable({ positionPagination: "both", // "top" | "bottom" | "both" | "none" }) ``` ## Search placement `positionGlobalFilter` moves the global search to the left region (next to the title/actions) or keeps it in the right-hand icon cluster. `"none"` hides it (same as `enableGlobalFilter: false`). ```tsx useDataTable({ positionGlobalFilter: "left", // "left" | "right" | "none" }) ``` ## Alert banner & drop zone The row-selection alert banner and the group-by drop zone default to the top. Reposition or hide them independently: ```tsx useDataTable({ positionToolbarAlertBanner: "bottom", // "top" | "bottom" | "head-overlay" | "none" positionToolbarDropZone: "both", // "top" | "bottom" | "both" | "none" }) ``` ## Styling Every toolbar slot exposes a `data-slot` attribute, so you can target it with a Tailwind selector instead of a styling prop — no per-slot props needed: | Slot | Type | Default | Description | | --- | --- | --- | --- | | `data-table-toolbar` | | | Top toolbar root. | | `data-table-toolbar-actions` | | | Internal actions cluster (search, filters, density, full screen, …). | | `data-table-alert-banner` | | | Selection alert banner (`data-position="head-overlay"` when it lies over the header row). | | `data-table-bottom-toolbar` | | | Bottom toolbar (custom actions and/or pagination). | | `data-table-pagination` | | | Pagination controls. | | `data-table-facet-filter` | | | Trigger button of a `DataTableFacetFilter` (dashed border while nothing is picked). | | `data-table-reset-filters` | | | The `DataTableResetFilters` button (rendered only while a column filter is set). | ```tsx ``` ## Related - [State management](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/state): The data table keeps all of its state in TanStack Table's store: the standard table state (sorting, filters, selection…) and its own UI state (density, editing, filter modes…). - [Density](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/density): Density controls the vertical padding of table rows and the padding around the toolbars. - [Global search](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/global-search): An expandable global search box appears in the toolbar by default (enableGlobalFilter: true). - [Export](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/export): The table ships no built-in export — the same stance as Material React Table — so the block carries no CSV or spreadsheet dependencies. --- # Part props (slotProps) Every part of the table — the scroll surface, rows, cells, toolbars, the search box, edit fields and more — accepts extra props through the `slotProps` option. Use it to add classes, styles, `data-*` attributes, ARIA attributes or event handlers without copying the component. ```tsx const table = useDataTable({ data, columns, slotProps: { surface: { className: "max-h-[600px]" }, bodyRow: ({ row }) => ({ className: row.original.overdue ? "bg-destructive/10" : undefined, }), bodyCell: { className: "tabular-nums" }, searchInput: { placeholder: "Search people…" }, }, }) ``` Each part takes either an object of props, or a function that receives the part's context (always including `table`) and returns props. Example (`slot-props`): ```tsx function SlotPropsExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { pagination: { pageSize: 10 } }, slotProps: { // Dim inactive people and mark pending ones with a left border. bodyRow: ({ row }) => ({ className: row.original.status === "inactive" ? "text-muted-foreground" : row.original.status === "pending" ? "border-l-2 border-l-amber-500" : undefined, }), headCell: { className: "text-[11px] uppercase tracking-wide" }, surface: { className: "max-h-[480px]" }, }, }) return } ``` ## How props combine Your props are merged with the table's own, so you add to the built-in behavior rather than replacing it: - `className` — combined with the table's classes (conflicting Tailwind classes resolve in your favor). - `style` — merged; your keys win. - Event handlers (`onClick`, `onKeyDown`, …) — both run, the table's first. - `ref` — both refs receive the element. - Anything else — yours replaces the table's. ## Column-level parts A column can target its own cells through `meta.slotProps`. It supports `headCell`, `filterCell`, `bodyCell`, `footerCell` and `editField`, and is applied after the table-level props: ```tsx const columns: DataTableColumnDef[] = [ { accessorKey: "salary", header: "Salary", meta: { slotProps: { bodyCell: ({ row }) => ({ className: row.original.salary > 100_000 ? "font-semibold" : undefined, }), editField: { inputMode: "numeric", min: 0 }, }, }, }, ] ``` ## Parts | Part | Type | Default | Description | | --- | --- | --- | --- | | `root` | `
props · { table }` | | The outer wrapper (``'s own props also land here). | | `surface` | `
props · { table }` | | The scroll container around the table. Bound its height here. | | `table` | ` props · { table }` | | The `
` element. | | `head` | ` props · { table }` | | ``. | | `headRow` | ` props · { headerGroup, table }` | | Each header row. | | `headCell` | ` props · { table }` | | ``. | | `bodyRow` | ` props · { row, rowIndex, table }` | | Each body row. | | `bodyCell` | ` props · { table }` | | ``. | | `footerRow` | ` props · { footerGroup, table }` | | Each footer row. | | `footerCell` | `` element. Always present. | | `tableFooterRef` | `React.RefObject` | | The `` element. `null` unless a column defines a `footer`. | | `searchInputRef` | `React.RefObject` | | The global-search ``. `null` until the search box is expanded. | > **Populated after mount:** Refs are `null` during the first render and on the server. Read them inside an > effect or an event handler, and guard against `null` for elements that may not > be rendered (see the notes above). ## Related - [Virtualization](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/virtualization): Virtualization renders only the cells currently in view, keeping large tables fast. - [Toolbar customization](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/toolbar): The table renders a top toolbar (title slot + your actions on the left, an icon-action cluster on the right) and a bottom toolbar (pagination). - [Table instance](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/table-instance): useDataTable returns the standard TanStack table instance with three additions: the table's UI state in table.getState() (density, editing, filter modes…), methods to change it on table itself, and the resolved options and DOM refs on table.dataTable. --- # Theming The data table is **style-agnostic**. It is built entirely on standard shadcn/ui primitive APIs, and those primitives are declared as registry dependencies. When you run `shadcn add`, the CLI installs those primitives in *your* configured style (Sera, Vega, Nova, Luma, …) and base color — so the table simply inherits your existing look with nothing to override. > **No table-specific theme:** There is no separate table-specific theme. Whatever your shadcn style and base color are, the table already matches them. ## The card The whole table — toolbars, table and pagination — sits inside your own shadcn `Card`, so its corners, border, shadow and background match the other cards in your app. Change your `Card` (or your `--radius` and `--card` tokens) and the table follows. To drop the frame, for example when the table already sits inside a card of yours, override the root through [`slotProps`](/docs/guides/slot-props): ```tsx useDataTable({ // … slotProps: { root: { className: "rounded-none bg-transparent shadow-none ring-0" }, }, }) ``` ## Design tokens The only styling the table introduces is two design tokens, injected into your `globals.css` via the registry's `cssVars`: - `--highlight` - `--highlight-foreground` They are used for filter-match highlighting and fall back to `--accent` / `--accent-foreground` when unset. To customize the highlight color, override them in your stylesheet: ```css :root { --highlight: oklch(0.95 0.12 95); --highlight-foreground: oklch(0.2 0 0); } .dark { --highlight: oklch(0.45 0.1 95); --highlight-foreground: oklch(0.98 0 0); } ``` See [Column filtering](/docs/guides/column-filtering) for where highlighting appears. ## Density and dark mode Density padding (rows and toolbars) is controlled by the component itself, not by your theme. Dark mode is whatever your app's theme provider sets — for example `next-themes` toggling the `.dark` class. The table reads the same tokens in either mode. ## Retheming To retheme, change your shadcn base color or CSS variables. Because the table consumes your primitives and tokens directly, it follows along — there are no table-specific changes to make. ## Related - [Installation](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/installation): The data table is distributed as a shadcn registry item. - [Column filtering](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/column-filtering): Each column can expose its own filter control in a per-column filter row. --- # Custom icons Every glyph is a named slot. Override any subset with the `icons` option, which accepts a partial. Defaults come from [Lucide](https://lucide.dev) (shadcn's default icon library), but you can swap in any icon library. Example (`custom-icons`): ```tsx function CustomIconsExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, icons: { sortAscending: RiSortAsc, sortDescending: RiSortDesc, columnActions: RiMoreFill, columnVisibility: RiEqualizerLine, }, initialState: { showColumnFilters: true, pagination: { pageSize: 10 } }, }) return } ``` Each value is a component that accepts a `className`, so any icon library works — here swapping in Remix Icon: ```tsx import { DataTable, useDataTable } from "@/components/ui/data-table" import { RiArrowDownLine, RiArrowUpLine, RiSearchLine } from "@remixicon/react" const table = useDataTable({ data, columns, getRowId: (row) => row.id, icons: { sortAscending: RiArrowUpLine, sortDescending: RiArrowDownLine, search: RiSearchLine, }, }) return ``` ## App-wide defaults For icon (and localization) defaults shared across many tables, wrap a subtree in `DataTableConfigProvider`. ```tsx import { DataTableConfigProvider } from "@/components/ui/data-table" import { RiSearchLine } from "@remixicon/react" {children} ``` ## Pin and clear icons Pinning and clearing have their own slots: `pinLeft`, `pinRight` (column menu), `pinTop`, `pinBottom` (row pin button), `clearSort` and `clearFilter` (column menu). The column pins default to the Lucide pin turned to point into the edge it pins to, the row pins to the plain pin, and the clear items to `ListX` and `FilterX`. If you only set the older slots, the new ones follow them, so existing overrides keep working: - `pinLeft`, `pinRight`, `pinTop` and `pinBottom` fall back to your `pin`, without rotation. - `clearSort` falls back to your `clearAll`, and `clearFilter` to your `filterOff`. Set the new slots to tell the two pin directions apart. Code that types a full `DataTableIcons` object (not a `Partial`) must add the six new keys. > **Precedence:** Values resolve in this order: built-in defaults → `DataTableConfigProvider` → > per-call options. Per-call `icons` always win. ## Related - [Icon slot reference](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/icons): Every glyph the table renders is a named slot. - [Localization](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/localization): The table ships in 38 languages. - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. --- # Localization The table ships in 38 languages. English is built in, and each of the other 37 is an optional registry item you install only when you need it. Every user-facing string can also be overridden one by one with the `localization` option. Example (`localization`): ```tsx function LocalizationExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, localization: SPANISH, initialState: { showColumnFilters: true, pagination: { pageSize: 10 } }, }) return } ``` ## Install a language Add the language after the table. Each one is its own registry item, named `data-table-locale-`: ```bash pnpm dlx shadcn@latest add https://monabbir-ahmmad.github.io/shadcn-react-table/r/data-table-locale-de.json ``` It writes one file, `components/ui/data-table/locales/de.ts`, and adds no npm packages. Install as many languages as you like. ## Use it on one table ```tsx import { DataTable, useDataTable } from "@/components/ui/data-table" import { dataTableLocalizationDe } from "@/components/ui/data-table/locales/de" const table = useDataTable({ data, columns, getRowId: (row) => row.id, localization: dataTableLocalizationDe, }) return ``` ## Use it app-wide To set the language for many tables at once, wrap a subtree in `DataTableConfigProvider`. A `localization` option on a single table still overrides the provider. ```tsx import { DataTableConfigProvider } from "@/components/ui/data-table" import { dataTableLocalizationDe } from "@/components/ui/data-table/locales/de" export function Providers({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` Example (`config-provider`): ```tsx function ConfigProviderShell() { // App-wide strings and icons for every table below the provider. return ( ) } ``` ## Change single strings A language is a plain object, so spread it and replace only the keys you want. Define the object outside the component: a new object on every render rebuilds the columns. ```tsx const localization = { ...dataTableLocalizationDe, rowsPerPage: "Einträge pro Seite", } function PeopleTable() { const table = useDataTable({ data, columns, localization }) return } ``` > **Partial overrides:** Without a language file you only need the keys you want to change — everything > else falls back to the built-in English. Function-valued entries, such as > `paginationRange: (start, end, total) => ...`, build a label from the current > values. ## Numbers and dates Dates follow the localization's `language`: date filters and the date picker show dates, month names and weekday names in that language, always in the Gregorian calendar. Every language file sets `language` for you. If you write your own localization, set `language` to a BCP 47 tag such as `"es"`. Numbers do not follow `language`. Each language file formats its own numbers inside its function keys (for example `1,000` in English and `1.000` in German). A custom localization that only sets `language` keeps English number formatting, so call `toLocaleString` with your tag in the function keys yourself: ```tsx rowsSelected: (selected, total) => `${selected.toLocaleString("es")} de ${total.toLocaleString("es")} filas seleccionadas`, ``` Numbers and dates come from the runtime's `Intl` data, so the server and the browser can differ slightly for some languages. ## Available languages | Language | Code | Export | | --------------------- | ------------ | ------------------------------- | | English (built in) | `en` | `defaultLocalization` | | Arabic | `ar` | `dataTableLocalizationAr` | | Azerbaijani | `az` | `dataTableLocalizationAz` | | Bulgarian | `bg` | `dataTableLocalizationBg` | | Czech | `cs` | `dataTableLocalizationCs` | | Danish | `da` | `dataTableLocalizationDa` | | German | `de` | `dataTableLocalizationDe` | | Greek | `el` | `dataTableLocalizationEl` | | Spanish | `es` | `dataTableLocalizationEs` | | Estonian | `et` | `dataTableLocalizationEt` | | Persian | `fa` | `dataTableLocalizationFa` | | Finnish | `fi` | `dataTableLocalizationFi` | | French | `fr` | `dataTableLocalizationFr` | | Hebrew | `he` | `dataTableLocalizationHe` | | Croatian | `hr` | `dataTableLocalizationHr` | | Hungarian | `hu` | `dataTableLocalizationHu` | | Armenian | `hy` | `dataTableLocalizationHy` | | Indonesian | `id` | `dataTableLocalizationId` | | Italian | `it` | `dataTableLocalizationIt` | | Japanese | `ja` | `dataTableLocalizationJa` | | Korean | `ko` | `dataTableLocalizationKo` | | Nepali | `ne` | `dataTableLocalizationNe` | | Dutch | `nl` | `dataTableLocalizationNl` | | Norwegian | `no` | `dataTableLocalizationNo` | | Polish | `pl` | `dataTableLocalizationPl` | | Portuguese | `pt` | `dataTableLocalizationPt` | | Portuguese (Brazil) | `pt-BR` | `dataTableLocalizationPtBR` | | Romanian | `ro` | `dataTableLocalizationRo` | | Russian | `ru` | `dataTableLocalizationRu` | | Slovak | `sk` | `dataTableLocalizationSk` | | Serbian (Cyrillic) | `sr-Cyrl-RS` | `dataTableLocalizationSrCyrlRS` | | Serbian (Latin) | `sr-Latn-RS` | `dataTableLocalizationSrLatnRS` | | Swedish | `sv` | `dataTableLocalizationSv` | | Turkish | `tr` | `dataTableLocalizationTr` | | Ukrainian | `uk` | `dataTableLocalizationUk` | | Vietnamese | `vi` | `dataTableLocalizationVi` | | Chinese (Simplified) | `zh-Hans` | `dataTableLocalizationZhHans` | | Chinese (Traditional) | `zh-Hant` | `dataTableLocalizationZhHant` | ## Translation quality The translations are adapted from [Material React Table](https://www.material-react-table.com) (MIT License, Copyright (c) 2022 Kevin Vandy). Strings that Material React Table does not have were added here, and those lines are marked `// needs review` in the file: a native speaker has not checked them yet. To fix one, edit the file in your project (it is your copy) and delete the comment. To fix it for everyone, send a pull request against the matching file under `packages/shadcn-react-table/src/components/data-table/locales/`. ## Related - [Localization key reference](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/localization): Every user-facing string flows through a localization table. - [Custom icons](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/custom-icons): Every glyph is a named slot. - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. --- # Right-to-left The table supports right-to-left languages such as Arabic, Hebrew and Persian. It follows the direction it renders in, so there is no option to set: put `dir="rtl"` on the table or on any ancestor, usually ``. Example (`rtl`): ```tsx function RtlExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, localization: dataTableLocalizationAr, enableColumnPinning: true, enableColumnResizing: true, initialState: { pagination: { pageSize: 10 }, columnPinning: { left: ["firstName"] }, }, }) // The table follows the direction it renders in; set `dir="rtl"` on it or // any ancestor (usually ). return (
) } ``` ```tsx import { DataTable, useDataTable } from "@/components/ui/data-table" import { dataTableLocalizationAr } from "@/components/ui/data-table/locales/ar" const table = useDataTable({ data, columns, getRowId: (row) => row.id, localization: dataTableLocalizationAr, }) return (
) ``` ## What changes - **Layout.** Columns run from right to left, cell text aligns to the start (the right), and the pagination and expand chevrons point the other way. - **Pinned columns.** `left` pins to the start of the table and `right` to its end. In a right-to-left table, `left`-pinned columns sit on the right edge. - **Column alignment.** A column's `meta.align` of `"left"` means the start of the cell and `"right"` means its end, so a numeric column aligned `"right"` lines up on the left in a right-to-left table. - **Resizing.** The resize handle sits on each column's end (left) edge, and dragging it outwards (to the left) widens the column. - **Keyboard navigation.** ArrowLeft moves to the next column and ArrowRight to the previous one, matching what you see. - **Column virtualization.** Scrolling works with the browser's right-to-left scroll offsets. ## Menus and popovers Menus, selects and date pickers render in a portal outside the table, so they take their direction from the page. If only part of the page is right-to-left, give the shadcn primitives the direction too, with Radix's `DirectionProvider` (from `radix-ui`) or Base UI's `DirectionProvider`: ```tsx import { Direction } from "radix-ui" function ArabicTable() { // …useDataTable as above return (
) } ``` Setting `dir="rtl"` on `` covers both the table and its portals. --- # Loading state Pass `isLoading: true` to render skeleton rows and a progress bar in place of the data. Flip it back to `false` to reveal the real rows. This is especially useful with server-side data, where you toggle it around each fetch. Example (`loading`): ```tsx function LoadingExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const [isLoading, setIsLoading] = React.useState(true) const table = useDataTable({ data, columns, getRowId: (row) => row.id, isLoading, initialState: { pagination: { pageSize: 10 } }, renderTopToolbarCustomActions: () => ( ), }) return } ``` ## Usage Drive `isLoading` from your own state and toggle it however you like — here with `useState` and a button in `renderTopToolbarCustomActions`: ```tsx const [isLoading, setIsLoading] = useState(false) const table = useDataTable({ data, columns, getRowId: (row) => row.id, isLoading, renderTopToolbarCustomActions: () => ( ), }) return ``` Example (`empty-state`): ```tsx function EmptyStateExample() { const columns = useUserColumns() const table = useDataTable({ data: [] as User[], columns, getRowId: (row) => row.id, renderEmpty: () => (
No people yet Invite your team to see them here.
), }) return } ``` ## Reading the state You can read the current value back from the table instance: ```tsx if (table.dataTable.isLoading) { // skeletons + progress bar are showing } ``` > **Pairs well with server-side data:** Set `isLoading` to `true` before a request and back to `false` when it > resolves so users always see the skeletons while new pages or filtered results > load. ## Saving state For an in-flight mutation (rather than an initial load), set `isSaving: true`. It shows the progress bar without replacing rows with skeletons, so the current data stays visible while the save runs. ```tsx useDataTable({ data, columns, getRowId: (row) => row.id, isSaving }) ``` ## Choosing which affordances show `isLoading` turns on three affordances together; override any of them independently: - `showProgressBars` — the top progress bar (default `isLoading || isSaving`). - `showSkeletons` — skeleton rows while the body is empty (default `isLoading`). The skeletons are drawn in each column, at the column's width and pinning, with bars of varying length. Pass `showSkeletons: true` to cover rows already shown too — handy while refetching — instead of dimming them. Style the bars with `slotProps.skeleton`. - `showLoadingOverlay` — a dimming overlay over existing rows (default `isLoading`). ```tsx useDataTable({ data, columns, getRowId: (row) => row.id, isLoading, showSkeletons: false, // keep rows visible, just dim + show the progress bar }) ``` Example (`skeletons`): ```tsx function SkeletonsExample() { const all = React.useMemo(() => baseUsers, []) const [data, setData] = React.useState(all) const [isLoading, setIsLoading] = React.useState(false) const [refetching, setRefetching] = React.useState(false) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowSelection: true, isLoading: isLoading || refetching, // While refetching, cover the rows already shown. showSkeletons: refetching || undefined, initialState: { pagination: { pageIndex: 0, pageSize: 8 } }, renderTopToolbarCustomActions: () => (
), }) return } ``` ## Relevant options | Name | Type | Default | Description | | --- | --- | --- | --- | | `isLoading` | `boolean` | `false` | Show the loading affordances (progress bar; skeletons when empty; a dimming overlay over existing rows). Toggle each independently below. | | `isSaving` | `boolean` | `false` | Show the progress bar for an in-flight save/mutation. Defaults the progress bar on without replacing rows with skeletons. | | `showProgressBars` | `boolean` | | Show the top progress bar. Default: `isLoading \|\| isSaving`. | | `showSkeletons` | `boolean` | | Show skeleton rows while the body is empty. Default: `isLoading`. Passing `true` also covers rows already shown with skeletons (e.g. while refetching), instead of dimming them. | | `showLoadingOverlay` | `boolean` | | Dim existing rows with an overlay while loading. Default: `isLoading`. | ## Related - [Server-side data](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/server-side): When your data lives on a server, hand pagination, sorting, and filtering off to it. --- # Server-side data When your data lives on a server, hand pagination, sorting, and filtering off to it. All TanStack `manual*` flags pass through. Example (`server-side`): ```tsx function ServerSideExample() { const all = React.useMemo(() => makeUsers(95), []) const [pageIndex, setPageIndex] = React.useState(0) const [isLoading, setIsLoading] = React.useState(false) // Simulate a paged server response with a short delay. const [page, setPage] = React.useState(() => all.slice(0, PAGE_SIZE)) React.useEffect(() => { // Simulated server fetch: flag loading, then resolve the page after a delay. // eslint-disable-next-line react-hooks/set-state-in-effect setIsLoading(true) const id = setTimeout(() => { setPage(all.slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE)) setIsLoading(false) }, 400) return () => clearTimeout(id) }, [all, pageIndex]) const columns = useUserColumns() const table = useDataTable({ data: page, columns, getRowId: (row) => row.id, isLoading, manualPagination: true, rowCount: all.length, state: { pagination: { pageIndex, pageSize: PAGE_SIZE } }, onPaginationChange: (updater) => { const next = typeof updater === "function" ? updater({ pageIndex, pageSize: PAGE_SIZE }) : updater setPageIndex(next.pageIndex) }, }) return } ``` ## Server pagination Set `manualPagination: true`, supply the total `rowCount`, control `state.pagination`, and handle `onPaginationChange`. Use `isLoading` to show a loading state while fetching. ```tsx import { useState } from "react" import { DataTable, useDataTable } from "@/components/ui/data-table" const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }) const { data, rowCount, isFetching } = useUsers(pagination) const table = useDataTable({ data, columns, getRowId: (row) => row.id, manualPagination: true, rowCount, state: { pagination }, onPaginationChange: setPagination, isLoading: isFetching, }) return ``` ## Server sorting and filtering The same pattern applies to other operations: - Sorting: `manualSorting: true` + `state.sorting` + `onSortingChange`. - Filtering: `manualFiltering: true` + `state.columnFilters` / `globalFilter`. ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, manualSorting: true, state: { sorting }, onSortingChange: setSorting, manualFiltering: true, }) ``` > **Facets are not auto-computed:** In manual filtering, faceted option lists and min/max values are not computed > from the data — the server supplies the rows, so you must provide facet > options yourself if you use them. ## With React Query Keep the table's pagination, sorting and search in React state, put them in the query key, and pass the result back as `data` and `rowCount`. `placeholderData: keepPreviousData` keeps the current page on screen while the next one loads; `showProgressBars` follows `isFetching`. Example (`react-query`): ```tsx function ReactQueryExample() { const [client] = React.useState(() => new QueryClient()) return ( ) } ``` ## Columns from the data When the columns depend on the response, build them with `useMemo` from the data's fields, so they change only when the data does. Example (`dynamic-columns`): ```tsx function DynamicColumnsExample() { // Columns come from the fetched data's fields. const [records, setRecords] = React.useState[]>([]) const [isLoading, setIsLoading] = React.useState(true) React.useEffect(() => { let live = true void delay(500).then(() => { if (!live) return setRecords( baseUsers.slice(0, 10).map((u) => ({ id: u.id, city: u.city, country: u.country, status: u.status, })) ) setIsLoading(false) }) return () => { live = false } }, []) const columns = React.useMemo>[]>( () => Object.keys(records[0] ?? {}).map((key) => ({ accessorKey: key, header: key[0]!.toUpperCase() + key.slice(1), })), [records] ) const table = useDataTable({ data: records, columns, getRowId: (row) => row.id!, isLoading, enablePagination: false, }) return } ``` ## Related - [Loading state](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/loading): Pass isLoading: true to render skeleton rows and a progress bar in place of the data. - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. --- # Production-ready tables A table over a real API: data fetched with React Query, paging, sorting and search done on the server, the table's state kept in one hook, and edits saved through a mutation. Example (`react-query`): ```tsx function ReactQueryExample() { const [client] = React.useState(() => new QueryClient()) return ( ) } ``` ## 1. Keep the server-facing state in one hook ```tsx import type { PaginationState, SortingState } from "@tanstack/react-table" function usePeopleTableState() { const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 25, }) const [sorting, setSorting] = React.useState([]) const [globalFilter, setGlobalFilter] = React.useState("") return { state: { pagination, sorting, globalFilter }, onPaginationChange: setPagination, onSortingChange: setSorting, onGlobalFilterChange: setGlobalFilter, // What the API needs, derived in one place. query: { page: pagination.pageIndex + 1, size: pagination.pageSize, sort: sorting[0], q: globalFilter, }, } } ``` Swap the `useState` calls for your router's search params to make the URL the source of truth. ## 2. Fetch with React Query ```tsx const tableState = usePeopleTableState() const people = useQuery({ queryKey: ["people", tableState.query], queryFn: () => api.people(tableState.query), placeholderData: keepPreviousData, // keep the current page while the next loads }) ``` ## 3. Hand both to the table ```tsx const table = useDataTable({ data: people.data?.rows ?? [], columns, getRowId: (row) => row.id, rowCount: people.data?.total ?? 0, manualPagination: true, manualSorting: true, manualFiltering: true, state: tableState.state, onPaginationChange: tableState.onPaginationChange, onSortingChange: tableState.onSortingChange, onGlobalFilterChange: tableState.onGlobalFilterChange, isLoading: people.isPending, // skeletons on the first load showProgressBars: people.isFetching, // a progress bar on later loads }) ``` With manual filtering, the search box waits for a pause in typing before it sets `globalFilter`, so each keystroke doesn't send a request. ## 4. Save edits through a mutation ```tsx const save = useMutation({ mutationFn: api.updatePerson, onSuccess: () => queryClient.invalidateQueries({ queryKey: ["people"] }), }) useDataTable({ // … enableEditing: true, editDisplayMode: "modal", isSaving: save.isPending, onEditingRowSave: async ({ row, values, exit }) => { await save.mutateAsync({ id: row.id, ...values }) exit() }, }) ``` `isSaving` shows the progress bar while the request runs; `exit()` closes the editor only once the save succeeds. ## Checklist - `getRowId` from your API's id. - Stable `columns` (module scope). - `manual*` for everything the server does, plus `rowCount`. - `isLoading` for the first load, `showProgressBars` for refetches, `isSaving` for writes. - Show errors (`people.isError`) with `renderEmpty` or above the table. ## Related - [Server-side data](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/server-side): When your data lives on a server, hand pagination, sorting, and filtering off to it. - [State management](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/state): The data table keeps all of its state in TanStack Table's store: the standard table state (sorting, filters, selection…) and its own UI state (density, editing, filter modes…). - [Best practices](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/best-practices): A few habits keep tables fast, typed and easy to change. --- # Pagination The table pages its rows by default, ten to a page, with a page-size menu and first / previous / next / last buttons below the rows. ## Page size and starting page ```tsx useDataTable({ data, columns, getRowId: (row) => row.id, pageSizeOptions: [10, 25, 50], initialState: { pagination: { pageIndex: 0, pageSize: 25 } }, }) ``` ## How the controls look - `paginationDisplayMode: "default"` — the row range ("1–10 of 48") with first / previous / next / last buttons. - `paginationDisplayMode: "pages"` — numbered page buttons. - `paginationDisplayMode: "custom"` — no built-in controls; render your own in `renderBottomToolbarCustomActions`, or place the exported `DataTablePagination` anywhere (see [Composition](/docs/guides/composition)). `positionPagination` puts the controls at the `"bottom"` (default), `"top"`, `"both"` or `"none"`. Example (`pagination-pages`): ```tsx function PaginationPagesExample() { const data = React.useMemo(() => makeUsers(120), []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, paginationDisplayMode: "pages", positionPagination: "both", pageSizeOptions: [5, 10, 20], initialState: { pagination: { pageSize: 5 } }, }) return } ``` ## Turning it off `enablePagination: false` shows every row. Pair it with a bounded surface and [virtualization](/docs/guides/virtualization) for long lists, or with [infinite scroll](/docs/guides/infinite-scroll). ## Server pagination Set `manualPagination: true`, pass the total `rowCount`, and control `state.pagination` with `onPaginationChange`; the table renders the page you give it. See [Server-side data](/docs/guides/server-side). ## Behavior worth knowing - Changing a filter or the search goes back to the first page. - When rows go away (a delete, a refetch) and the current page no longer exists, the table moves to the last page that does — except while `isLoading`, so a refetch doesn't jump pages. - The range text and page numbers follow the table's language (`localization.language`). ## Relevant options | Name | Type | Default | Description | | --- | --- | --- | --- | | `enablePagination` | `boolean` | `true` | Splits rows into pages with pagination controls. Default `true`; turn it off for virtualization or infinite scroll. | | `pageSizeOptions` | `number[]` | `DEFAULT_PAGE_SIZE_OPTIONS` | Choices in the rows-per-page select. Default `[5, 10, 25, 50, 100]`. | | `paginationDisplayMode` | `PaginationDisplayMode` | `"default"` | Pagination control style: `"default"` (range label + first/prev/next/last buttons), `"pages"` (numbered page buttons), or `"custom"` (render your own via `renderBottomToolbarCustomActions`). Default "default". | | `positionPagination` | `"top" \| "bottom" \| "both" \| "none"` | `"bottom"` | Where the pagination controls render. Default "bottom". "none" keeps pagination active but hides the controls. | ## Related - [Server-side data](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/server-side): When your data lives on a server, hand pagination, sorting, and filtering off to it. - [Infinite scroll](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/infinite-scroll): Infinite scroll loads more rows as the user scrolls toward the bottom, instead of paging through the data. - [Display modes](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/display-modes): Several features expose a *DisplayMode option that changes _how_ the UI is presented without changing the underlying behavior. --- # Infinite scroll Infinite scroll loads more rows as the user scrolls toward the bottom, instead of paging through the data. Set `enableInfiniteScroll: true` and drive it with three controlled props: `onLoadMore`, `hasNextPage`, and `isFetchingNextPage`. Example (`infinite-scroll`): ```tsx function InfiniteScrollExample() { const all = React.useMemo(() => makeUsers(INFINITE_TOTAL), []) const [rows, setRows] = React.useState(() => all.slice(0, INFINITE_CHUNK)) const [isFetchingNextPage, setIsFetchingNextPage] = React.useState(false) const hasNextPage = rows.length < all.length // Stable loader (memoized on `all`) so the table doesn't re-trigger every // render. The hook already gates on hasNextPage / isFetchingNextPage. const loadMore = React.useCallback(() => { setIsFetchingNextPage(true) // Simulate a server round-trip, then append the next chunk. setTimeout(() => { setRows((prev) => all.slice(0, Math.min(prev.length + INFINITE_CHUNK, all.length)) ) setIsFetchingNextPage(false) }, 600) }, [all]) const columns = useUserColumns() const table = useDataTable({ slotProps: { surface: { className: "max-h-[460px]" } }, data: rows, columns, getRowId: (row) => row.id, enableInfiniteScroll: true, hasNextPage, isFetchingNextPage, onLoadMore: loadMore, renderTopToolbarCustomActions: () => ( Loaded {rows.length} of {all.length} ), }) return } ``` When infinite scroll is on, the pager is hidden and pagination is forced to a single growing page, so every appended row renders. The table watches a sentinel near the bottom of the scroll surface and calls `onLoadMore` only when the sentinel nears the viewport **and** `hasNextPage` is true **and** a fetch is not already in flight. ## Controlled fetch state You own the data and the loading flags; the table only decides *when* to ask for more. Append the next chunk to `data` in your `onLoadMore` handler. ```tsx import { DataTable, useDataTable } from "@/components/ui/data-table" function Table() { const [rows, setRows] = React.useState(() => firstPage) const [isFetchingNextPage, setIsFetchingNextPage] = React.useState(false) const hasNextPage = rows.length < total // Memoize so the table doesn't re-trigger on every render. const loadMore = React.useCallback(() => { setIsFetchingNextPage(true) fetchNextPage(rows.length).then((next) => { setRows((prev) => [...prev, ...next]) setIsFetchingNextPage(false) }) }, [rows.length]) const table = useDataTable({ data: rows, columns, getRowId: (row) => row.id, enableInfiniteScroll: true, hasNextPage, isFetchingNextPage, onLoadMore: loadMore, }) return } ``` > **Pairs with TanStack Query:** The contract maps one-to-one onto TanStack Query's `useInfiniteQuery`: pass > `fetchNextPage` as `onLoadMore`, and forward `hasNextPage` / > `isFetchingNextPage` straight through. ## Prefetch distance `infiniteScrollThreshold` (default `200`) is the distance in pixels from the bottom at which the next chunk is requested, so data arrives before the user hits the end. ```tsx const table = useDataTable({ data: rows, columns, enableInfiniteScroll: true, hasNextPage, isFetchingNextPage, onLoadMore: loadMore, infiniteScrollThreshold: 400, }) ``` ## Works with or without virtualization The sentinel sits after the rendered window, so infinite scroll works whether or not row virtualization is on. For very large loaded datasets, pair it with `enableRowVirtualization` so only the visible rows render. ```tsx const table = useDataTable({ data: rows, columns, enableInfiniteScroll: true, enableRowVirtualization: true, hasNextPage, isFetchingNextPage, onLoadMore: loadMore, }) ``` > **Reset on sort / filter changes:** In server mode, resetting the query (on a new sort or filter) is your job: > replace `data` with the first page again. The sentinel re-observes the new > rows automatically. ## Relevant options | Name | Type | Default | Description | | --- | --- | --- | --- | | `enableInfiniteScroll` | `boolean` | `false` | Load more rows as the user scrolls near the bottom (infinite append). When on, the pager is hidden and pagination is forced to a single growing page, so all appended rows render. Fully controlled: the table calls `onLoadMore` only when the bottom sentinel nears the viewport AND `hasNextPage` AND `!isFetchingNextPage`. Pairs directly with TanStack Query's `useInfiniteQuery` (`fetchNextPage`/`hasNextPage`/`isFetchingNextPage`). Default false. | | `onLoadMore` | `() => void` | | Called to request the next chunk. Should be stable (memoized) so the table doesn't re-trigger on every render. Append the result to `data`. | | `hasNextPage` | `boolean` | `false` | Whether more rows remain to be loaded. Default false. | | `isFetchingNextPage` | `boolean` | `false` | Whether a load is currently in flight (blocks re-triggering). Default false. | | `infiniteScrollThreshold` | `number` | `200` | Distance in px from the bottom at which to prefetch. Default 200. | ## Related - [Server-side data](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/server-side): When your data lives on a server, hand pagination, sorting, and filtering off to it. - [Virtualization](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/virtualization): Virtualization renders only the cells currently in view, keeping large tables fast. - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. --- # Virtualization Virtualization renders only the cells currently in view, keeping large tables fast. The data table supports both row and column virtualization. ## Row virtualization Set `enableRowVirtualization: true` to render only the visible rows. This is recommended once you have more than roughly 100 rows. Example (`virtualized`): ```tsx function VirtualizedExample() { const data = React.useMemo(() => makeUsers(1000), []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enablePagination: false, enableRowVirtualization: true, }) return } ``` With row virtualization on, the table surface becomes the scroll container, so give it a bounded height via `slotProps.surface` (it defaults to a viewport-based height). ```tsx import { DataTable, useDataTable } from "@/components/ui/data-table" const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowVirtualization: true, slotProps: { surface: { className: "max-h-[600px]" } }, estimateRowHeight: 52, virtualOverscan: 8, }) return ``` Tune the behavior with `estimateRowHeight` (default `52`) and `virtualOverscan` (default `8`, the number of extra rows rendered above and below the viewport). ## Advanced virtualizer control For finer control, both virtualizers expose the underlying [`@tanstack/react-virtual`](https://tanstack.com/virtual) API. Pass `rowVirtualizerOptions` / `columnVirtualizerOptions` to merge extra options into the `useVirtualizer` call. They override the built-in `count`, `estimateSize`, `overscan`, and `measureElement`, and accept either an object or a `({ table }) => options` function: ```tsx const table = useDataTable({ data, columns, enableRowVirtualization: true, rowVirtualizerOptions: { overscan: 12, gap: 4 }, }) ``` To drive the virtualizer imperatively (for example, to scroll to a row), pass a ref via `rowVirtualizerInstanceRef` / `columnVirtualizerInstanceRef`: ```tsx import { useRef } from "react" import type { DataTableRowVirtualizer } from "@/components/ui/data-table" const rowVirtualizerRef = useRef(null) const table = useDataTable({ data, columns, enableRowVirtualization: true, rowVirtualizerInstanceRef: rowVirtualizerRef, }) // later, e.g. in an effect or event handler: rowVirtualizerRef.current?.scrollToIndex(100) ``` > **Dragging rows:** Rows can be dragged with virtualization on (`enableRowOrdering`, > `enableRowDragging`): the table scrolls as you drag toward its edge, and the > dragged row stays rendered. See > [Row ordering](/docs/guides/row-ordering#with-virtualization). ## Column virtualization For very wide tables, set `enableColumnVirtualization: true` to virtualize columns. This applies fixed column widths. Column pinning (pinned columns stay in place while you scroll sideways), column ordering and grouped headers all work with it. Example (`column-virtualization`): ```tsx function ColumnVirtualizationExample() { const data = React.useMemo(() => makeUsers(300), []) const columns = React.useMemo(() => wideColumns(), []) const table = useDataTable({ slotProps: { surface: { className: "max-h-[600px]" } }, data, columns, getRowId: (row) => row.id, enablePagination: false, enableRowVirtualization: true, enableColumnVirtualization: true, }) return } ``` ```tsx import { DataTable, useDataTable } from "@/components/ui/data-table" const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableColumnVirtualization: true, enablePagination: false, }) return ``` > **Pair with no pagination:** Column virtualization is usually paired with `enablePagination: false` so all > rows scroll within one virtualized surface. ## Relevant options | Name | Type | Default | Description | | --- | --- | --- | --- | | `enableRowVirtualization` | `boolean` | `false` | Virtualize body rows for large datasets (recommended past ~100 rows). The table surface becomes the scroll container — give it a bounded height via `className`/`style` (defaults to `max-h-[600px]`). Rows can still be dragged: the dragged row stays rendered while the drag scrolls. | | `enableColumnVirtualization` | `boolean` | `false` | Virtualize columns for very wide tables. Applies fixed column widths; works with column pinning (pinned columns stay rendered), column ordering and grouped headers. | | `estimateRowHeight` | `number` | `52` | Estimated row height (px) for the virtualizer. Default 52. | | `virtualOverscan` | `number` | `8` | Extra rows rendered above/below the viewport. Default 8. | | `rowVirtualizerOptions` | `RowVirtualizerOptions` | | Partial `@tanstack/react-virtual` options merged into the row virtualizer (overrides the built-in `count`/`estimateSize`/`overscan`/`measureElement`). A `rangeExtractor` picks the rendered rows; a row being dragged is added to its result. Accepts an object or a `({ table }) => options` function. | | `columnVirtualizerOptions` | `ColumnVirtualizerOptions` | | Partial `@tanstack/react-virtual` options merged into the column virtualizer. Accepts an object or a `({ table }) => options` function. | | `rowVirtualizerInstanceRef` | `React.RefObject` | | Ref populated with the row `Virtualizer` instance for imperative control (e.g. `scrollToIndex`). Only set when `enableRowVirtualization`. | | `columnVirtualizerInstanceRef` | `React.RefObject` | | Ref populated with the column `Virtualizer` instance. Only set when `enableColumnVirtualization`. | ## Related - [Row ordering](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/row-ordering): Set enableRowOrdering: true to add a drag-handle column for drag-and-drop row reordering. - [DataTable props](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/data-table-props): is mostly driven by the instance from useDataTable. - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. --- # Row height By default rows size to their content (and the virtualizer measures them). For explicit control, set a flat `rowHeight` or a per-row `getRowHeight`. Example (`auto-row-height`): ```tsx function AutoRowHeightExample() { const data = React.useMemo(() => baseUsers, []) const columns = React.useMemo[]>(() => { const bio: DataTableColumnDef = { id: "bio", header: "Bio", size: 300, accessorFn: (row) => `${row.firstName} ${row.lastName} is a ${row.role} in ${row.department}, ` + `based in ${row.city}, ${row.country}. Reach them at ${row.email}.`, cell: ({ getValue }) => {getValue()}, } return [...userColumns().slice(0, 3), bio] }, []) // getRowHeight: "auto" wraps and grows each row — even with resizing on. const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableColumnResizing: true, getRowHeight: () => "auto", initialState: { pagination: { pageSize: 8 } }, }) return } ``` ## Auto height (wrap & grow) Return `"auto"` from `getRowHeight` to let a row wrap its content and grow to fit — even when column resizing is on (which otherwise clips cells to a single line). ```tsx import { DataTable, useDataTable } from "@/components/ui/data-table" const table = useDataTable({ data, columns, enableColumnResizing: true, getRowHeight: () => "auto", }) return ``` ## Fixed height A number pins the row to that pixel height and clips overflowing content. Use a flat `rowHeight` for every row, or return a number per row from `getRowHeight`. ```tsx const table = useDataTable({ data, columns, rowHeight: 64, // every row is 64px }) ``` ```tsx const table = useDataTable({ data, columns, // Tall rows only where there's extra content; default height elsewhere. getRowHeight: (row) => (row.original.notes ? "auto" : 44), }) ``` Example (`fixed-row-height`): ```tsx function FixedRowHeightExample() { const data = React.useMemo(() => makeUsers(200), []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enablePagination: false, enableRowVirtualization: true, rowHeight: 36, slotProps: { surface: { className: "max-h-[420px]" } }, }) return } ``` ## Precedence For each row: `getRowHeight(row)` wins unless it returns `null`/`undefined`, then `rowHeight`, then the density default. > **Works with virtualization:** Per-row heights feed the row virtualizer's estimate; `"auto"` rows are > measured after render, so large virtualized datasets with variable heights > scroll accurately. See [Virtualization](/docs/guides/virtualization). > **Per row, not per column:** `getRowHeight` sets the height of the whole row. There's no per-column wrap > toggle — an `"auto"` row wraps all of its cells. ## Relevant options | Name | Type | Default | Description | | --- | --- | --- | --- | | `rowHeight` | `number` | | Flat height (px) applied to every row. Overridden per-row by `getRowHeight`. Also seeds the virtualizer estimate. | | `getRowHeight` | `(row: DataTableRow) => number \| "auto" \| null` | | Per-row height. Return a px number to pin the row, `"auto"` to let it wrap and grow to fit its content (even with column resizing on), or `null` to fall back to `rowHeight` / the density default. Applies to data rows. | | `estimateRowHeight` | `number` | `52` | Estimated row height (px) for the virtualizer. Default 52. | ## Related - [Virtualization](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/virtualization): Virtualization renders only the cells currently in view, keeping large tables fast. - [Density](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/density): Density controls the vertical padding of table rows and the padding around the toolbars. - [useDataTable options](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/use-data-table): useDataTable(options) accepts everything TanStack's TableOptions accepts (controlled state, on*Change, manual* flags, getRowId, getSubRows, initialState, …; column pinning in state/initialState keeps the left/right shape) plus the data-table options below. --- # Export The table ships no built-in export — the same stance as Material React Table — so the block carries no CSV or spreadsheet dependencies. Everything an exporter needs is already on the table instance: pick the rows, map them through the visible columns, and hand the result to whichever writer you prefer. Example (`export`): ```tsx function ExportExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowSelection: true, renderTopToolbarCustomActions: ({ table }) => ( ), initialState: { pagination: { pageSize: 10 } }, }) return } ``` ## Reading the data Pick a row scope from the table's row models, then build an array-of-arrays (header + rows) from the visible data columns: ```tsx import type { DataTableInstance } from "@/components/ui/data-table" function toRows(table: DataTableInstance) { // Selected rows when any are selected, else the filtered set (all pages). const selected = table.getSelectedRowModel().rows const rows = selected.length > 0 ? selected : table.getFilteredRowModel().rows // Data columns only — injected columns (selection, actions, …) have no accessor. const columns = table .getVisibleLeafColumns() .filter((column) => column.accessorFn != null) return [ columns.map((column) => column.id), ...rows.map((row) => columns.map((column) => row.getValue(column.id))), ] } ``` Other scopes: `table.getRowModel().rows` (current page) and `table.getPreFilteredRowModel().rows` (everything, ignoring filters). ## Writing a CSV No library needed for CSV. Two details matter: quote cells containing delimiters, and neutralize formula injection (spreadsheets execute cells starting with `=`, `+`, `-`, `@`, tab, or CR when a CSV is opened): ```tsx function toCsv(rows: unknown[][]) { const escape = (value: unknown) => { const text = value == null ? "" : String(value) const guarded = /^[=+\-@\t\r]/.test(text) ? `'${text}` : text return /[",\n]/.test(guarded) ? `"${guarded.replace(/"/g, '""')}"` : guarded } return rows.map((row) => row.map(escape).join(",")).join("\n") } function downloadCsv(csv: string, fileName: string) { // The BOM makes Excel detect UTF-8. const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8;" }) const url = URL.createObjectURL(blob) const link = document.createElement("a") link.href = url link.download = `${fileName}.csv` link.click() URL.revokeObjectURL(url) } ``` Wire it to the toolbar with `renderTopToolbarCustomActions`: ```tsx const table = useDataTable({ data, columns, enableRowSelection: true, renderTopToolbarCustomActions: ({ table }) => ( ), }) ``` ## Excel and richer formats For `.xlsx`, feed the same array-of-arrays to [SheetJS](https://docs.sheetjs.com/docs/getting-started/installation/nodejs) (`XLSX.utils.aoa_to_sheet` → `XLSX.writeFile`) — note SheetJS is installed from their CDN tarball, not npm. For heavier CSV needs (custom delimiters, streaming), [PapaParse](https://www.papaparse.com/)'s `unparse` accepts the same shape. Example (`export-pdf`): ```tsx function ExportPdfExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { pagination: { pageIndex: 0, pageSize: 10 } }, renderTopToolbarCustomActions: ({ table }) => ( ), }) return } ``` ## Related - [Row selection](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/row-selection): Set enableRowSelection: true to add a leading checkbox column. - [Toolbar customization](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/toolbar): The table renders a top toolbar (title slot + your actions on the left, an icon-action cluster on the right) and a bottom toolbar (pagination). --- # State management The data table keeps all of its state in TanStack Table's store: the standard table state (sorting, filters, selection…) and its own UI state (density, editing, filter modes…). Every slice can be left uncontrolled, observed, or fully controlled, and parts of the table re-render only for the slices they show. ## TanStack table state All the standard table state — sorting, column filters, global filter, pagination, row selection, column visibility, ordering, pinning, sizing, expansion, grouping — is plain TanStack Table state. `useDataTable` spreads its options straight into TanStack's `useTable`, so you control it exactly as you would in TanStack: pass `state` + the matching `onXChange` setter (and optionally `initialState` for uncontrolled seeds). ```tsx const [sorting, setSorting] = React.useState([]) const [rowSelection, setRowSelection] = React.useState({}) const table = useDataTable({ data, columns, getRowId: (row) => row.id, state: { sorting, rowSelection }, onSortingChange: setSorting, onRowSelectionChange: setRowSelection, }) ``` For server-side data, combine this with the `manual*` flags — see [Server-side data](/docs/guides/server-side). ## Data table UI state The state the table adds on top of TanStack is table state too: **density**, **full screen**, **filter-row visibility**, whether the **search box** is open, the **global search mode**, **column filter modes**, the **advanced filter** and its dialog, and **inline editing** (the cell or row being edited, the create form, the draft). It sits in `table.getState()` next to sorting, takes `initialState` and controlled `state` like any slice, and reports changes through `on…Change` callbacks with the new value. ```tsx const [editingCell, setEditingCell] = React.useState(null) const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { density: "compact", showColumnFilters: true }, state: { editingCell }, onEditingCellChange: setEditingCell, }) ``` Observe a slice without taking it over with its `on…Change` callback: ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { density: "compact" }, onDensityChange: (density) => saveToLocalStorage("density", density), }) ``` | Name | Type | Default | Description | | --- | --- | --- | --- | | `onDensityChange` | `(density: Density) => void` | | Called whenever the density changes (toolbar toggle or programmatic). Set it with `initialState.density`, control it with `state.density`. | | `onIsFullscreenChange` | `(isFullscreen: boolean) => void` | | Called whenever the full-screen state is toggled. Control it with `state.isFullscreen`. | | `onShowColumnFiltersChange` | `(showColumnFilters: boolean) => void` | | Called whenever the filter row is shown or hidden. Set it with `initialState.showColumnFilters`, control it with `state.showColumnFilters`. | | `onGlobalFilterModeChange` | `(mode: GlobalFilterMode) => void` | | Called whenever the global search mode changes (default `"fuzzy"`). Set it with `initialState.globalFilterMode`, control it with `state.globalFilterMode`. | | `onColumnFilterModesChange` | `(modes: Record) => void` | | Called when a column's filter mode changes, with every column's mode. Control it with `state.columnFilterModes`. | | `onShowAdvancedFilterPanelChange` | `(open: boolean) => void` | | Called when the advanced filter dialog opens or closes. Control it with `state.showAdvancedFilterPanel`. | | `onEditingCellChange` | `(cell: EditingCell \| null) => void` | | Called when cell editing starts or ends (`null`). Control it with `state.editingCell`. | | `onEditingRowIdChange` | `(rowId: string \| null) => void` | | Called when row editing starts or ends (`null`). Control it with `state.editingRowId`. | | `onIsCreatingChange` | `(isCreating: boolean) => void` | | Called when the new-row editor opens or closes. Control it with `state.isCreating`. | | `onRowDraftChange` | `(draft: Record) => void` | | Called when the draft of the row being edited or created changes. Control it with `state.rowDraft`. | Example (`controlled-state`): ```tsx function ControlledStateExample() { const data = React.useMemo(() => baseUsers, []) const columns = useUserColumns() const [sorting, setSorting] = React.useState([ { id: "salary", desc: true }, ]) const [density, setDensity] = React.useState("compact") const table = useDataTable({ data, columns, getRowId: (row) => row.id, initialState: { pagination: { pageSize: 10 } }, state: { sorting, density }, onSortingChange: setSorting, onDensityChange: setDensity, }) return (
{JSON.stringify({ sorting, density })}
) } ``` ## Reading state in your component `` subscribes to the state it shows. The component that calls `useDataTable` does **not** re-render when the table's state changes, so code there that reads the state during render would show an old value. Tell `useDataTable` what your render reads with `stateSelector`; the component then re-renders when that part changes (compared shallowly): ```tsx const table = useDataTable({ data, columns, getRowId: (row) => row.id, enableRowSelection: true, stateSelector: (state) => state.rowSelection, }) const selectedCount = Object.keys(table.getState().rowSelection).length ``` To re-render only a small part of your component, use `table.Subscribe` instead: ```tsx state.rowSelection}> {(rowSelection) => {Object.keys(rowSelection).length} selected} ``` Event handlers, effects and the table's own render props (`renderTopToolbarCustomActions`, `renderBottomToolbarCustomActions`, …) always see the current state; only reads during your component's render need this. ## Reading and setting from the instance Whether controlled or not, the current values are in `table.getState()` and the setters are methods on `table`: ```tsx table.getState().density // "comfortable" | "compact" | "spacious" table.setDensity("compact") table.setIsFullscreen(true) table.setShowColumnFilters((prev) => !prev) ``` > **Persisting state:** Mirror any of these into local storage or a query param, then restore via the > matching `default*` (uncontrolled) or controlled value on the next load. ## Styling without props The table is styled with Tailwind, not per-component style props. Structural slots emit a `data-slot` attribute you can target with a selector — see the table in [Toolbar customization](/docs/guides/toolbar#styling). ## Related - [Toolbar customization](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/toolbar): The table renders a top toolbar (title slot + your actions on the left, an icon-action cluster on the right) and a bottom toolbar (pagination). - [Server-side data](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/server-side): When your data lives on a server, hand pagination, sorting, and filtering off to it. - [Table instance](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/api/table-instance): useDataTable returns the standard TanStack table instance with three additions: the table's UI state in table.getState() (density, editing, filter modes…), methods to change it on table itself, and the resolved options and DOM refs on table.dataTable. --- # Best practices A few habits keep tables fast, typed and easy to change. ## Keep `data` and `columns` stable The table rebuilds its rows when `data` or `columns` change identity. Define columns at module scope (or in `useMemo`), and keep data in state, a memo or your query cache: ```tsx // Module scope: created once. const columns: DataTableColumnDef[] = [ { accessorKey: "name", header: "Name" }, ] function People() { const { data = [] } = usePeopleQuery() // stable, from the cache const table = useDataTable({ data, columns, getRowId: (row) => row.id }) return } ``` Avoid building them inline — this rebuilds the table on every render: ```tsx useDataTable({ data: people.filter(isActive), columns: [{ accessorKey: "name", header: "Name" }], }) ``` The same goes for `displayColumnDefOptions` and the custom function maps (`filterFns`, `sortFns`, `aggregationFns`). ## Always pass `getRowId` Selection, editing, pinning, drag and drop and the row-state optimizations all key rows by id. Without `getRowId` rows are keyed by index, so selecting row 3 and then sorting selects someone else. ## Type columns with `DataTableColumnDef` ```tsx import type { DataTableColumnDef } from "@/components/ui/data-table" const columns: DataTableColumnDef[] = [ { accessorKey: "age", header: "Age", meta: { filterVariant: "range" } }, ] ``` `accessorKey` is checked against your row type, `meta` knows every column option of the table, and render callbacks get the full table (`table.getState()`, `table.dataTable`, the methods). ## Read state where you render it The component that calls `useDataTable` doesn't re-render when the table's state changes — that's what keeps big tables fast. When it shows state, subscribe to just that part: ```tsx state.rowSelection}> {(selection) => {Object.keys(selection).length} selected} ``` Event handlers, effects and the table's own render options can call `table.getState()` directly. ## Control only what you need Pass `initialState` for starting values, and `state` + `on…Change` only for the slices your app owns (the URL, a server query). Everything else can stay inside the table. ## Style through `slotProps` Add classes and attributes with [`slotProps`](/docs/guides/slot-props) instead of editing the installed files, so updating the block stays a re-install. ## Related - [State management](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/state): The data table keeps all of its state in TanStack Table's store: the standard table state (sorting, filters, selection…) and its own UI state (density, editing, filter modes…). - [Production-ready tables](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/production-ready): A table over a real API: data fetched with React Query, paging, sorting and search done on the server, the table's state kept in one hook, and edits saved through a mutation. - [Updating](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/updating): shadcn add copies this block's source into your project — after that, the code is yours, and upstream releases don't reach it automatically. --- # Accessibility & keyboard The table is built to be used without a mouse and with a screen reader. Its parts are checked with [axe](https://github.com/dequelabs/axe-core) in a real browser in CI. ## The grid With `enableKeyboardNavigation` (on by default) the table is an ARIA grid: one Tab stop for the whole body, `aria-rowcount` / `aria-colcount` for the full data (also when rows or columns are virtualized), and `aria-rowindex` / `aria-colindex` on every row and cell. Turn it off and the table is a plain `
props · { header, column, table }` | | Each header cell. | | `filterCell` | ` props · { header, column, table }` | | Each cell of the column filter row. | | `body` | `
props · { cell, row, column, table }` | | Each body cell. | | `detailPanel` | ` props · { row, table }` | | The cell holding an expanded row's detail panel. | | `footer` | `
props · { header, column, table }` | | Each footer cell. | | `topToolbar` | `
props · { table }` | | The toolbar above the table. | | `bottomToolbar` | `
props · { table }` | | The toolbar below the table. | | `pagination` | `
props · { table }` | | The pagination controls. | | `alertBanner` | `
props · { table }` | | The selection banner. | | `searchInput` | ` props · { table }` | | The global search input. | | `editField` | ` props · { column, row?, table }` | | Each inline / dialog edit field (the input, or the select's trigger). | | `editDialog` | `
props · { row?, isCreating, table }` | | The edit / create dialog's content. | | `skeleton` | `
props · { column, rowIndex, table }` | | Each loading skeleton (bar or square) in a body cell. | | `selectCheckbox` | `
`. | Keys | What they do | | --- | --- | | Arrow keys | Move between cells (left and right are mirrored in right-to-left tables) | | Home / End | First / last cell in the row | | Ctrl + Home / Ctrl + End | First / last cell in the table | | Page Up / Page Down | Up / down a screenful of rows | | Enter or F2 | Activate the cell: open its editor, toggle its checkbox, follow its link | | Ctrl + C (Cmd + C) | Copy the focused cell's text | | Escape | Cancel an edit; leave full screen | Rows and columns that aren't rendered (virtualization) are scrolled into view as you reach them. Focus comes back to the same cell after an edit is saved or cancelled, and to the nearest cell when its row is deleted. ## Headers and controls - Sort buttons report `aria-sort`; menus, filters and toggles are real buttons with names from `localization`. - **Resize a column** from the keyboard: Tab to its handle (a separator that announces the width), then the arrow keys (10px, or 50px with Shift), Home / End for the limits, Enter to fit. - **Reorder** columns and rows from the keyboard: focus the drag handle, Space to pick it up, the arrow keys to move, Space to drop, Escape to cancel. Each step is announced in the table's language, naming columns by their label and rows by their position. - The columns the table adds itself (selection, actions, expand, row pin, drag) have names for screen readers even when they show no header text. ## State changes - Selecting rows updates a polite status banner; loading is announced the same way. - Full screen is a modal dialog: focus stays inside, the page doesn't scroll behind it, and Escape leaves it with focus still in the table. - Selection uses checkboxes, with a mixed state on select-all for "some selected". With single selection, checking a row unchecks the one before it. ## Motion and language - The progress bar and the column-drag slide respect "reduce motion". - 38 languages, right-to-left included (see [Localization](/docs/guides/localization) and [Right-to-left](/docs/guides/rtl)); numbers and dates follow the language. ## What you can do - Give each table a name with `slotProps: { table: { "aria-label": "People" } }` (a `title` shows above the table but doesn't name the grid). - Keep custom `cell` renderers keyboard-friendly: Enter activates the first focusable control in a cell. - Check your own colors: contrast comes from your theme, so it isn't part of the table's checks. ## Related - [Row selection](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/row-selection): Set enableRowSelection: true to add a leading checkbox column. - [Column resizing](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/column-resizing): Set enableColumnResizing: true to add a drag handle on each column's edge. - [Column ordering](https://monabbir-ahmmad.github.io/shadcn-react-table/docs/guides/column-ordering): Set enableColumnOrdering: true to add a drag grip to each header. --- # useDataTable options `useDataTable(options)` accepts everything TanStack's [`TableOptions`](https://tanstack.com/table/latest) accepts (controlled `state`, `on*Change`, `manual*` flags, `getRowId`, `getSubRows`, `initialState`, …; column pinning in `state`/`initialState` keeps the `left`/`right` shape) **plus** the data-table options below. The table below is generated from the source types; defaults come from the hook implementation. | Option | Type | Default | Description | | --- | --- | --- | --- | | `initialState` | `Omit< NonNullable["initialState"]>, "columnPinning" \| "pagination" > & { columnPinning?: DataTableColumnPinningState pagination?: Partial }` | | Initial values for uncontrolled table state. `pagination` may be partial (TanStack fills the rest from its defaults); column pinning uses `left` / `right`. | | `state` | `Omit< NonNullable["state"]>, "columnPinning" > & { columnPinning?: DataTableColumnPinningState }` | | Controlled table state slices. Column pinning uses `left` / `right` (not TanStack v9's `start` / `end`). | | `onColumnPinningChange` | `OnChangeFn` | | Called with an updater over the `left` / `right` pinning state when the user pins or unpins a column. | | `stateSelector` | `(state: DataTableState) => unknown` | | Re-render the component that calls `useDataTable` when the selected part of the table state changes (compared shallowly). By default it does not re-render on table state changes at all: `` subscribes to what it shows. Select what your own render reads, e.g. `(state) => state.rowSelection` for a "3 selected" label. | | `localization` | `Partial` | | Text for every label, message and screen-reader string. Partial: missing keys fall back to English (or to `DataTableConfigProvider`). Install a locale item for a full language. | | `icons` | `Partial` | | Override any subset of the table's icons. | | `isLoading` | `boolean` | `false` | Show the loading affordances (progress bar; skeletons when empty; a dimming overlay over existing rows). Toggle each independently below. | | `isSaving` | `boolean` | `false` | Show the progress bar for an in-flight save/mutation. Defaults the progress bar on without replacing rows with skeletons. | | `showProgressBars` | `boolean` | | Show the top progress bar. Default: `isLoading \|\| isSaving`. | | `showSkeletons` | `boolean` | | Show skeleton rows while the body is empty. Default: `isLoading`. Passing `true` also covers rows already shown with skeletons (e.g. while refetching), instead of dimming them. | | `showLoadingOverlay` | `boolean` | | Dim existing rows with an overlay while loading. Default: `isLoading`. | | `enableFacetedValues` | `boolean` | `true` | Compute faceted unique values / min-max (auto select options + range bounds). Default true; disable to skip the faceted row models. | | `enableColumnActions` | `boolean` | `true` | Shows the column-actions menu in each header (sort, filter, pin, hide, group…). Default `true`. | | `enableColumnFilterModes` | `boolean` | `true` | Show the filter-mode menu adornment on filter fields. Default true. | | `enableFilterMatchHighlighting` | `boolean` | `true` | Highlight matched substrings in cells. Default true. | | `enableGlobalFilter` | `boolean` | `true` | Show the expandable global search in the toolbar. Default true. | | `enableGlobalFilterModes` | `boolean` | `true` | Show the global search mode menu (fuzzy/contains/…). Default true. | | `filterFns` | `Record>` | `NO_FILTER_FNS` | Your own filter functions, by name (TanStack's filter function signature). Name one on a column's `filterFn`, as a column's `meta.filterMode`, or in `columnFilterModeOptions` / `globalFilterModeOptions` to offer it in the mode menus (labelled by `localization.filterModes[name]`, else the name). The names are read when the table is created; the functions stay current. | | `sortFns` | `Record>` | | Your own sort functions, by name; a column uses one with `sortFn: "name"`. The names are read when the table is created; the functions stay current. | | `aggregationFns` | `Record>` | | Your own aggregation functions, by name; a column uses one with `aggregationFn: "name"`. The names are read when the table is created; the functions stay current. | | `columnFilterModeOptions` | `FilterMode[]` | | Restrict (and order) every column's filter-mode menu to these modes — built-in ones (kept where the column's variant supports them) and custom `filterFns` names. A column's `meta.columnFilterModeOptions` wins. | | `globalFilterModeOptions` | `GlobalFilterMode[]` | `GLOBAL_FILTER_MODES as GlobalFilterMode[]` | Restrict (and order) the global search mode menu: built-in modes and custom `filterFns` names. | | `enableGlobalFilterRankedResults` | `boolean` | `false` | While the global search is in fuzzy mode, order rows by best match (most relevant first) until the user applies their own sort. Default false — MRT defaults this on, but the data table keeps it off so searching never silently reorders rows unless opted in. Ignored for non-fuzzy modes, when grouping or expanded, or under manual sorting/filtering. | | `enableBatchRowSelection` | `boolean` | `true` | Shift-click a row's checkbox to select (or clear) every row since the last one clicked. Default true. | | `displayColumnDefOptions` | `Partial< Record>> >` | | Column options for the columns the table adds itself (selection, expand, row actions, row number, row pin, drag handle), by id — e.g. `{ "cn-select": { size: 48 } }`. `meta` merges with the column's own. | | `defaultDisplayColumn` | `Partial>` | | Column options applied to every column the table adds itself, under `displayColumnDefOptions`. | | `slotProps` | `DataTableSlots` | | Props for the table's parts (cells, rows, toolbars, …), as an object or a function of the part's context. `className` is combined with the table's, `style` merged, event handlers both run; other props replace the table's. See `DataTableSlots`. | | `pageSizeOptions` | `number[]` | `DEFAULT_PAGE_SIZE_OPTIONS` | Choices in the rows-per-page select. Default `[5, 10, 25, 50, 100]`. | | `onDensityChange` | `(density: Density) => void` | | Called whenever the density changes (toolbar toggle or programmatic). Set it with `initialState.density`, control it with `state.density`. | | `onIsFullscreenChange` | `(isFullscreen: boolean) => void` | | Called whenever the full-screen state is toggled. Control it with `state.isFullscreen`. | | `onShowColumnFiltersChange` | `(showColumnFilters: boolean) => void` | | Called whenever the filter row is shown or hidden. Set it with `initialState.showColumnFilters`, control it with `state.showColumnFilters`. | | `onShowGlobalFilterChange` | `(showGlobalFilter: boolean) => void` | | Called whenever the global search box opens or closes. Set it with `initialState.showGlobalFilter`, control it with `state.showGlobalFilter`. | | `enableAdvancedFilter` | `boolean` | `false` | Enable the advanced filter panel: compound rules joined by AND/OR, applied on top of the per-column filters. A toolbar button opens the panel and shows the active-rule count. Default false. | | `onAdvancedFilterChange` | `(filter: AdvancedFilterGroup) => void` | | Called whenever the advanced filter changes. Set it with `initialState.advancedFilter`, control it with `state.advancedFilter`. | | `onGlobalFilterModeChange` | `(mode: GlobalFilterMode) => void` | | Called whenever the global search mode changes (default `"fuzzy"`). Set it with `initialState.globalFilterMode`, control it with `state.globalFilterMode`. | | `onColumnFilterModesChange` | `(modes: Record) => void` | | Called when a column's filter mode changes, with every column's mode. Control it with `state.columnFilterModes`. | | `onShowAdvancedFilterPanelChange` | `(open: boolean) => void` | | Called when the advanced filter dialog opens or closes. Control it with `state.showAdvancedFilterPanel`. | | `onEditingCellChange` | `(cell: EditingCell \| null) => void` | | Called when cell editing starts or ends (`null`). Control it with `state.editingCell`. | | `onEditingRowIdChange` | `(rowId: string \| null) => void` | | Called when row editing starts or ends (`null`). Control it with `state.editingRowId`. | | `onIsCreatingChange` | `(isCreating: boolean) => void` | | Called when the new-row editor opens or closes. Control it with `state.isCreating`. | | `onRowDraftChange` | `(draft: Record) => void` | | Called when the draft of the row being edited or created changes. Control it with `state.rowDraft`. | | `enableColumnOrdering` | `boolean` | `false` | Drag-and-drop column reordering (adds a grip to each header). | | `enableColumnPinning` | `boolean` | `false` | Column pinning (left/right) via the column-actions menu + sticky columns. | | `enableColumnResizing` | `boolean` | `false` | Column resizing via an edge drag handle. | | `layoutMode` | `LayoutMode` | `"semantic"` | How the table sizes its columns. `"semantic"` (default) uses the browser's table layout. `"grid"` keeps every column at its size and shares the spare width between the growing columns by weight (see the column's `meta.grow`; the table's own columns never grow), so the table fills its container. `"grid-no-grow"` keeps every column at its size and the table as wide as its columns. | | `enableColumnAutosize` | `boolean` | | Double-clicking a column's resize handle auto-sizes it to fit its widest value. Defaults to `enableColumnResizing`. When off, double-click resets the column to its default size instead. | | `enableRowOrdering` | `boolean` | `false` | Drag-and-drop row reordering (adds a drag-handle column). | | `enableRowPinning` | `boolean` | `false` | Row pinning (top) via a pin toggle in the row-number column. | | `enableRowNumbers` | `boolean` | `false` | Adds a leading row-number column. | | `rowNumberMode` | `"static" \| "original"` | `"static"` | "static" tracks the current view (page-aware); "original" uses source index. | | `enableRowDragging` | `boolean` | `false` | Adds a drag handle to each row for moving rows out of the table — to another table or a drop zone in the same `DataTableDndProvider` — without reordering inside it (combine with `enableRowOrdering` for both). Default false. | | `onRowDragEnd` | `(props: { row: DataTableRow table: DataTableInstance over: RowDropTarget }) => void` | | Called when a row is dropped on another table or a drop zone in the same `DataTableDndProvider`. Move the row in your data. | | `onRowOrderChange` | `(activeRowId: string, overRowId: string) => void` | | Called on a row drag-and-drop reorder; the consumer reorders its data. | | `enableGrouping` | `boolean` | `false` | Row grouping (group-by menu + drop-to-group zone + aggregated group rows). | | `enableExpanding` | `boolean` | | Row expansion (tree sub-rows and/or detail panels). Auto-on with grouping or when `renderDetailPanel`/`getSubRows` is provided. | | `enableStickyFooter` | `boolean` | `false` | Pin the footer (aggregation/footer cells) to the bottom of the surface. Default false. | | `rowPinningDisplayMode` | `RowPinningDisplayMode` | `"top-and-bottom"` | Where pinned rows show and how rows get pinned (see {@link RowPinningDisplayMode}). Default `"top-and-bottom"`. | | `enableTableHead` | `boolean` | `true` | Render the header rows. Default true; `false` shows the body only. | | `enableTableFooter` | `boolean` | `true` | Render the footer rows when columns define a `footer`. Default true. | | `enableExpandAll` | `boolean` | `true` | Show the expand-all toggle in the expand column's header. Default true. | | `renderDetailPanel` | `(props: { row: DataTableRow table: DataTableInstance }) => React.ReactNode` | | Render an expanding detail panel for each row. | | `enableEditing` | `boolean \| ((row: DataTableRow) => boolean)` | `false` | Enable inline editing, or decide per row with a function (rows it returns false for can't be edited; creating rows is still allowed). | | `editDisplayMode` | `EditDisplayMode` | `"cell"` | How edits are surfaced. Default "cell". | | `createDisplayMode` | `CreateDisplayMode` | `"modal"` | How the create form is surfaced, independent of `editDisplayMode`: `"modal"` (dialog, default), `"row"` (an inline editable row at the top of the table), or `"custom"` (you render it from `isCreating` + `rowDraft`). | | `createRowDefaults` | `Record` | | Default values for the create form, keyed by column id. | | `enableClickToCopy` | `ClickToCopyOption` | `false` | Copy cells on click (`true`), from the cell menu (`"context-menu"`), or decide per cell with a function. Per-column override via meta. | | `enableCellActions` | `boolean \| ((cell: DataTableCell) => boolean)` | `false` | A right-click menu on each cell with built-in Copy (when click-to-copy is on) and Edit (when the cell is editable) items, or a function deciding per cell. `renderCellActionMenuItems` customizes it. Default false. | | `onEditingCellSave` | `(props: { row: DataTableRow column: DataTableColumn value: unknown table: DataTableInstance }) => void` | | Called when a cell edit is committed (cell and table editing) with the new `value`. Persist it; the table does not change `data` itself. | | `onEditingRowSave` | `(props: { row: DataTableRow values: Record table: DataTableInstance exit: () => void }) => void` | | Called when a row edit is saved (row and modal editing) with the edited `values`. Call `exit()` to leave editing, e.g. after the save succeeds. | | `onCreatingRowSave` | `(props: { values: Record table: DataTableInstance exit: () => void }) => void` | | Called when the new-row editor is saved with the row's `values`. Call `exit()` to close it. | | `onEditingRowCancel` | `(props: { row: DataTableRow table: DataTableInstance }) => void` | | Called when the user cancels a row edit (Cancel, closing the dialog). | | `onCreatingRowCancel` | `(props: { table: DataTableInstance }) => void` | | Called when the user cancels the create form. | | `renderEditRowDialogContent` | `( props: EditDialogContentProps & { row: DataTableRow } ) => React.ReactNode` | | Replace the edit dialog's content. You get the built-in fields as `internalEditComponents`, plus `save()` / `cancel()`. | | `renderCreateRowDialogContent` | `( props: EditDialogContentProps ) => React.ReactNode` | | Replace the create dialog's content (same props, without `row`). | | `positionCreatingRow` | `"top" \| "bottom" \| number` | `"top"` | Where the inline create row (`createDisplayMode: "row"`) appears: `"top"` (default), `"bottom"`, or a row index on the current page to insert it before (with row virtualization an index means `"top"`). | | `renderRowActions` | `(props: { row: DataTableRow table: DataTableInstance }) => React.ReactNode` | | Content of the row-actions column, replacing the default edit and menu buttons. | | `renderCellActionMenuItems` | `(props: { cell: DataTableCell row: DataTableRow table: DataTableInstance /** The built-in items (Copy, Edit) the menu shows for this cell. */ internalMenuItems: React.ReactNode closeMenu: () => void }) => React.ReactNode` | | The contents of the right-click menu on each cell (turns the menu on). Include `internalMenuItems` to keep the built-in Copy / Edit items. | | `renderRowActionMenuItems` | `(props: { row: DataTableRow table: DataTableInstance /** The built-in items (Edit, when the row can be edited). */ internalMenuItems: React.ReactNode closeMenu: () => void }) => React.ReactNode` | | Render a kebab menu in the row-actions column. Returns the menu items (e.g. ``); injects the actions column automatically. While editing is on, the Edit action moves into this menu: include `internalMenuItems` to keep it. | | `renderColumnActionsMenuItems` | `(props: { column: DataTableColumn table: DataTableInstance /** The built-in items (sort, filter, hide, group, pin). */ internalMenuItems: React.ReactNode closeMenu: () => void }) => React.ReactNode` | | The contents of every column-actions menu. Return your items (e.g. ``) with `internalMenuItems` where the built-in ones should go, or leave it out to replace them. | | `renderColumnFilterModeMenuItems` | `(props: { column: DataTableColumn modes: FilterMode[] currentMode: FilterMode onSelect: (mode: FilterMode) => void table: DataTableInstance }) => React.ReactNode` | | Replace the radio items in a column's filter-mode menu. Render your own items and call `onSelect(mode)` to switch; `modes` is the allowed set. | | `renderGlobalFilterModeMenuItems` | `(props: { modes: GlobalFilterMode[] currentMode: GlobalFilterMode onSelect: (mode: GlobalFilterMode) => void table: DataTableInstance }) => React.ReactNode` | | Replace the radio items in the global-search mode menu. Render your own items and call `onSelect(mode)` to switch; `modes` is the allowed set. | | `onRowClick` | `(props: RowEvent) => void` | | Fired when a body row is clicked / double-clicked. | | `onRowDoubleClick` | `(props: RowEvent) => void` | | Called when a row is double-clicked. Double-clicks on controls inside the row (edit, expand, pin…) are ignored. | | `onCellClick` | `(props: CellEvent) => void` | | Fired when a body cell is clicked / double-clicked. | | `onCellDoubleClick` | `(props: CellEvent) => void` | | Called when a cell is double-clicked. Double-clicks on controls inside the cell are ignored. | | `enableRowVirtualization` | `boolean` | `false` | Virtualize body rows for large datasets (recommended past ~100 rows). The table surface becomes the scroll container — give it a bounded height via `className`/`style` (defaults to `max-h-[600px]`). Rows can still be dragged: the dragged row stays rendered while the drag scrolls. | | `enableColumnVirtualization` | `boolean` | `false` | Virtualize columns for very wide tables. Applies fixed column widths; works with column pinning (pinned columns stay rendered), column ordering and grouped headers. | | `estimateRowHeight` | `number` | `52` | Estimated row height (px) for the virtualizer. Default 52. | | `virtualOverscan` | `number` | `8` | Extra rows rendered above/below the viewport. Default 8. | | `rowHeight` | `number` | | Flat height (px) applied to every row. Overridden per-row by `getRowHeight`. Also seeds the virtualizer estimate. | | `getRowHeight` | `(row: DataTableRow) => number \| "auto" \| null` | | Per-row height. Return a px number to pin the row, `"auto"` to let it wrap and grow to fit its content (even with column resizing on), or `null` to fall back to `rowHeight` / the density default. Applies to data rows. | | `rowVirtualizerOptions` | `RowVirtualizerOptions` | | Partial `@tanstack/react-virtual` options merged into the row virtualizer (overrides the built-in `count`/`estimateSize`/`overscan`/`measureElement`). A `rangeExtractor` picks the rendered rows; a row being dragged is added to its result. Accepts an object or a `({ table }) => options` function. | | `columnVirtualizerOptions` | `ColumnVirtualizerOptions` | | Partial `@tanstack/react-virtual` options merged into the column virtualizer. Accepts an object or a `({ table }) => options` function. | | `rowVirtualizerInstanceRef` | `React.RefObject` | | Ref populated with the row `Virtualizer` instance for imperative control (e.g. `scrollToIndex`). Only set when `enableRowVirtualization`. | | `columnVirtualizerInstanceRef` | `React.RefObject` | | Ref populated with the column `Virtualizer` instance. Only set when `enableColumnVirtualization`. | | `enableStickyHeader` | `boolean` | `true` | Pin the header rows to the top of the scrollable surface. Also gives the surface a default viewport-bound max-height (`clamp(350px, 100dvh - 200px, 9999px)`) so tall content scrolls internally; override the bound with `slotProps.surface`. Default true. | | `enablePagination` | `boolean` | `true` | Splits rows into pages with pagination controls. Default `true`; turn it off for virtualization or infinite scroll. | | `enableInfiniteScroll` | `boolean` | `false` | Load more rows as the user scrolls near the bottom (infinite append). When on, the pager is hidden and pagination is forced to a single growing page, so all appended rows render. Fully controlled: the table calls `onLoadMore` only when the bottom sentinel nears the viewport AND `hasNextPage` AND `!isFetchingNextPage`. Pairs directly with TanStack Query's `useInfiniteQuery` (`fetchNextPage`/`hasNextPage`/`isFetchingNextPage`). Default false. | | `onLoadMore` | `() => void` | | Called to request the next chunk. Should be stable (memoized) so the table doesn't re-trigger on every render. Append the result to `data`. | | `hasNextPage` | `boolean` | `false` | Whether more rows remain to be loaded. Default false. | | `isFetchingNextPage` | `boolean` | `false` | Whether a load is currently in flight (blocks re-triggering). Default false. | | `infiniteScrollThreshold` | `number` | `200` | Distance in px from the bottom at which to prefetch. Default 200. | | `positionPagination` | `"top" \| "bottom" \| "both" \| "none"` | `"bottom"` | Where the pagination controls render. Default "bottom". "none" keeps pagination active but hides the controls. | | `paginationDisplayMode` | `PaginationDisplayMode` | `"default"` | Pagination control style: `"default"` (range label + first/prev/next/last buttons), `"pages"` (numbered page buttons), or `"custom"` (render your own via `renderBottomToolbarCustomActions`). Default "default". | | `columnFilterDisplayMode` | `ColumnFilterDisplayMode` | `"subheader"` | Where column filter inputs live: `"subheader"` (a filter row under the header, default), `"popover"` (per-column popovers opened from the column header), or `"custom"` (you render them). | | `positionGlobalFilter` | `"left" \| "right" \| "none"` | `"right"` | Which toolbar region the global search renders in. Default "right" (the internal-actions cluster). "left" places it next to the title/actions; "none" hides it (same as `enableGlobalFilter: false`). | | `positionToolbarAlertBanner` | `"top" \| "bottom" \| "head-overlay" \| "none"` | `"top"` | Where the row-selection alert banner renders: above the table (`"top"`, default), below it (`"bottom"`), over the header row (`"head-overlay"`, with the select-all checkbox, so selecting rows does not move the table; needs the table head), or nowhere (`"none"`). | | `positionToolbarDropZone` | `"top" \| "bottom" \| "both" \| "none"` | `"top"` | Where the group-by drop zone renders (grouping only). Default "top". | | `positionActionsColumn` | `"first" \| "last"` | `"last"` | Position of the auto-injected row-actions column. Default "last". | | `positionExpandColumn` | `"first" \| "last"` | `"first"` | Position of the auto-injected expand column (tree / detail panel). Default "first". | | `selectAllMode` | `"page" \| "all"` | `"page"` | Select-all scope for the header checkbox: the current page ("page", default) or every row ("all"). | | `enableSelectAll` | `boolean` | `true` | Show the select-all checkbox in the selection column header. Default true. | | `enableTopToolbar` | `boolean` | `true` | Shows the toolbar above the table (title, search, filters, view options). Default `true`. | | `enableBottomToolbar` | `boolean` | `true` | Shows the toolbar below the table (pagination, custom actions). Default `true`. | | `enableDensityToggle` | `boolean` | `true` | Show the density toggle in the toolbar. Default true. | | `enableFullscreenToggle` | `boolean` | `true` | Show the full-screen toggle in the toolbar. Default true. | | `enableToolbarInternalActions` | `boolean` | `true` | Show the toolbar's internal icon-action cluster (search, filters, column visibility, density, full screen). Default true. Hides the whole cluster at once; use the per-item flags for finer control. | | `enableKeyboardNavigation` | `boolean` | `true` | Cell keyboard navigation with a single Tab stop: arrow keys, Home/End, PageUp/PageDown, Enter or F2 to activate a cell, Ctrl+C to copy it. Also makes the table an ARIA grid. Default `true`. | | `enableAnimations` | `boolean` | `true` | Animate the table: rows glide when their order changes, fade in and out when they enter or leave the data, detail panels open and close smoothly, and the sort arrow, selection checkboxes and selected count transition. Always off when the user prefers reduced motion. Retime it with the CSS variables `--dt-motion-duration`, `--dt-motion-duration-fast` and `--dt-motion-ease` on the table. | | `title` | `React.ReactNode` | | Heading shown at the start of the top toolbar. | | `renderTopToolbarCustomActions` | `( props: DataTableSlotProps ) => React.ReactNode` | | Custom content rendered in the top toolbar's left region (next to the title), e.g. bulk-action buttons. | | `renderTopToolbar` | `(props: DataTableSlotProps) => React.ReactNode` | | Replace the entire top toolbar with custom content. | | `renderBottomToolbar` | `(props: DataTableSlotProps) => React.ReactNode` | | Replace the entire bottom toolbar (pagination region) with custom content. | | `renderToolbarInternalActions` | `( props: DataTableSlotProps ) => React.ReactNode` | | Replace the top toolbar's internal icon-action cluster with custom content. | | `renderBottomToolbarCustomActions` | `( props: DataTableSlotProps ) => React.ReactNode` | | Custom content rendered in the bottom toolbar's left region (next to pagination), e.g. summary text or actions. | | `renderCaption` | `(props: DataTableSlotProps) => React.ReactNode` | | Render a `
` for the table (e.g. an accessible summary). | | `renderEmpty` | `(props: DataTableSlotProps) => React.ReactNode` | | Content shown when there are no rows. Defaults to `localization.noRecordsToDisplay`. | > **Pass-through options:** Options not listed here (e.g. `data`, `columns`, `state`, `manualPagination`, `rowCount`, `enableRowSelection`, `enableMultiSort`) are standard TanStack Table options and behave exactly as documented there. --- # Column options Per-column configuration lives on the standard TanStack `columnDef.meta` object, which the table augments with the fields below (fully typed wherever `meta` is read). ```tsx const columns: DataTableColumnDef[] = [ { accessorKey: "role", header: "Role", meta: { filterVariant: "select", filterSelectOptions: ROLE_OPTIONS, editVariant: "select", }, }, ] ``` | meta key | Type | Default | Description | | --- | --- | --- | --- | | `filterVariant` | `FilterVariant` | | Filter UI variant rendered in the filter row. Defaults to "text". | | `filterSelectOptions` | `DataTableFilterOption[]` | | Options for `select` / `multi-select` filter variants. If omitted for a select-style variant, options are derived from faceted unique values. | | `filterMode` | `FilterMode` | | Default filter mode for this column (overrides the per-variant default). | | `enableColumnFilterModes` | `boolean` | | Per-column override for the filter-mode menu (defaults to the table). | | `renderColumnFilter` | `(props: { column: Column table: DataTableInstance }) => React.ReactNode` | | Custom filter UI for this column (escape hatch). Replaces the variant. | | `columnFilterModeOptions` | `FilterMode[]` | | Restrict (and order) the filter-mode menu for this column to this subset of modes (built-in ones and custom `filterFns` names). Include the column's default mode. | | `enableEditing` | `boolean \| ((row: DataTableRow) => boolean)` | | Allow editing this column (defaults to true when table editing is on), or decide per row with a function. | | `editVariant` | `EditVariant` | | Inline editor variant. Defaults to "text". | | `editSelectOptions` | `\| DataTableFilterOption[] \| ((props: { row?: DataTableRow column: DataTableColumn table: DataTableInstance }) => DataTableFilterOption[])` | | Options for the "select" edit variant, or a function returning them for the row being edited (`row` is undefined in the create form). | | `renderEditCell` | `(props: CellRenderProps) => React.ReactNode` | | Custom inline editor for this column (escape hatch). Replaces the built-in editor while the cell/row is editing; read the draft from `table.getState().rowDraft` and write it with `table.setRowDraftValue` (row editing), or save with `onEditingCellSave` (cell editing). | | `renderGroupedCell` | `( props: CellRenderProps ) => React.ReactNode` | | Custom render for this column's group header cell (when grouped). | | `renderAggregatedCell` | `( props: CellRenderProps ) => React.ReactNode` | | Custom render for this column's aggregated cell (when grouped). Overrides the TanStack `columnDef.aggregatedCell`. | | `renderPlaceholderCell` | `( props: CellRenderProps ) => React.ReactNode` | | Custom render for this column's placeholder cells in grouped rows (cells with no value because another column owns the group). Default: empty. | | `validate` | `(value: unknown) => string \| undefined` | | Validate an edited value; return an error message or undefined if valid. | | `enableClickToCopy` | `ClickToCopyOption` | | Copy this column's cells on click (`true`), from the cell menu (`"context-menu"`), or decide per cell with a function. | | `align` | `"left" \| "center" \| "right"` | | Horizontal alignment applied to the header label and body cells. | | `grow` | `boolean \| number` | | In `layoutMode: "grid"`, whether this column takes a share of the spare width (`true`, the default for data columns), keeps its size (`false` or 0), or how large its share is (a number: 2 takes twice the share of a column with 1). A column the user has resized (one with a size in `columnSizing`) keeps that size. | | `enableFilterMatchHighlighting` | `boolean` | | `false` opts this column out of match highlighting (the table option of the same name turns it on or off for every column). | | `visibleInShowHideMenu` | `boolean` | | `false` leaves this column out of the show / hide columns menu. | | `enableColumnActions` | `boolean` | | `false` hides the column-actions menu for this column. | | `label` | `string` | | Human-readable label for menus when the header is not a plain string. | | `slotProps` | `Pick< DataTableSlots, "headCell" \| "filterCell" \| "bodyCell" \| "footerCell" \| "editField" >` | | Props for this column's cells, applied after the table's `slotProps` for the same part. | > **Standard column options still apply:** `accessorKey`, `header`, `cell`, `footer`, `size`, `enableSorting`, > `enableColumnFilter`, `aggregationFn`, `aggregatedCell`, `sortDescFirst`, and > the rest are standard TanStack [column > defs](https://tanstack.com/table/latest). --- # DataTable props `` is mostly driven by the instance from `useDataTable`. It accepts the presentational props below, and spreads any other `div` props onto the root element. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `table` | `DataTableInstance` | | The instance returned by `useDataTable`. | ```tsx ``` > **Styling inner parts:** To reach the scroll surface, rows, cells or any other part, use the [`slotProps`](/docs/guides/slot-props) option on `useDataTable` — for example `slotProps: { surface: { className: "max-h-[600px]" } }` bounds the scroll surface for a [sticky header](/docs/guides/sticky) or a [virtualized](/docs/guides/virtualization) list. --- # Table instance `useDataTable` returns the standard TanStack table instance with three additions: the table's UI state in `table.getState()` (density, editing, filter modes…), methods to change it on `table` itself, and the resolved options and DOM refs on **`table.dataTable`**. ```tsx table.beginCreate() // open the create-row form table.setDensity("compact") const density = table.getState().density const isLoading = table.dataTable.isLoading table.dataTable.refs.searchInputRef.current?.focus() ``` ## Methods | Method | Type | Default | Description | | --- | --- | --- | --- | | `setDensity` | `React.Dispatch>` | | Updates `density`. | | `setIsFullscreen` | `React.Dispatch>` | | Updates `isFullscreen`. | | `setShowColumnFilters` | `React.Dispatch>` | | Updates `showColumnFilters`. | | `setShowGlobalFilter` | `React.Dispatch>` | | Open or close the global search box. | | `setColumnFilterMode` | `(columnId: string, mode: FilterMode) => void` | | Switch a column's filter mode (resets the value when it becomes invalid). | | `setGlobalFilterMode` | `(mode: GlobalFilterMode) => void` | | Updates `globalFilterMode`. | | `setAdvancedFilter` | `React.Dispatch>` | | Updates `advancedFilter`. | | `setShowAdvancedFilterPanel` | `React.Dispatch>` | | Updates `showAdvancedFilterPanel`. | | `autoSizeColumn` | `(columnId: string) => void` | | Resize a single column to fit its widest visible value (header + data). | | `autoSizeAllColumns` | `() => void` | | Auto-size every resizable visible column. | | `setEditingCell` | `React.Dispatch>` | | Updates `editingCell`. | | `setRowDraftValue` | `(columnId: string, value: unknown) => void` | | Updates `rowDraftValue`. | | `beginRowEdit` | `(row: DataTableRow) => void` | | Enter row/modal editing for a row, seeding the draft from its values. | | `beginCreate` | `() => void` | | Open the create form, seeding the draft from `createRowDefaults`. | | `cancelEdit` | `() => void` | | Exit any editing/creating state, discarding the draft. | ## `table.dataTable` | Member | Type | Default | Description | | --- | --- | --- | --- | | `localization` | `DataTableLocalization` | | Text for every label, message and screen-reader string. Partial: missing keys fall back to English (or to `DataTableConfigProvider`). Install a locale item for a full language. | | `icons` | `DataTableIcons` | | Override any subset of the table's icons. | | `filterFns` | `Record>` | | The consumer's own filter functions, by name. | | `columnFilterModeOptions` | `FilterMode[] \| undefined` | | Table-wide filter-mode menu modes, if restricted. | | `globalFilterModeOptions` | `GlobalFilterMode[]` | | The global search mode menu's modes. | | `enableGlobalFilter` | `boolean` | | Show the expandable global search in the toolbar. Default true. | | `enableGlobalFilterModes` | `boolean` | | Show the global search mode menu (fuzzy/contains/…). Default true. | | `enableAdvancedFilter` | `boolean` | | Compound AND/OR filter panel (independent of, and additive to, the per-column filters and global search). | | `isLoading` | `boolean` | | Show the loading affordances (progress bar; skeletons when empty; a dimming overlay over existing rows). Toggle each independently below. | | `isSaving` | `boolean` | | Show the progress bar for an in-flight save/mutation. Defaults the progress bar on without replacing rows with skeletons. | | `showProgressBars` | `boolean` | | Show the top progress bar. Default: `isLoading \|\| isSaving`. | | `showSkeletons` | `boolean` | | Show skeleton rows while the body is empty. Default: `isLoading`. Passing `true` also covers rows already shown with skeletons (e.g. while refetching), instead of dimming them. | | `showSkeletonsOverRows` | `boolean` | | Whether skeletons also cover rows already shown (`showSkeletons: true` was passed explicitly). | | `showLoadingOverlay` | `boolean` | | Dim existing rows with an overlay while loading. Default: `isLoading`. | | `enableFacetedValues` | `boolean` | | Compute faceted unique values / min-max (auto select options + range bounds). Default true; disable to skip the faceted row models. | | `enableColumnActions` | `boolean` | | Shows the column-actions menu in each header (sort, filter, pin, hide, group…). Default `true`. | | `enableColumnFilters` | `boolean` | | Whether per-column filters are on. | | `enableColumnFilterModes` | `boolean` | | Show the filter-mode menu adornment on filter fields. Default true. | | `enableFilterMatchHighlighting` | `boolean` | | Highlight matched substrings in cells. Default true. | | `enableColumnOrdering` | `boolean` | | Drag-and-drop column reordering (adds a grip to each header). | | `enableColumnPinning` | `boolean` | | Column pinning (left/right) via the column-actions menu + sticky columns. | | `enableColumnResizing` | `boolean` | | Column resizing via an edge drag handle. | | `layoutMode` | `LayoutMode` | | How the table sizes its columns (see {@link LayoutMode}). | | `enableColumnAutosize` | `boolean` | | Double-clicking a column's resize handle auto-sizes it to fit its widest value. Defaults to `enableColumnResizing`. When off, double-click resets the column to its default size instead. | | `enableRowOrdering` | `boolean` | | Drag-and-drop row reordering (adds a drag-handle column). | | `enableRowPinning` | `boolean` | | Row pinning (top) via a pin toggle in the row-number column. | | `enableRowNumbers` | `boolean` | | Adds a leading row-number column. | | `rowNumberMode` | `"static" \| "original"` | | "static" tracks the current view (page-aware); "original" uses source index. | | `onRowOrderChange` | `(activeRowId: string, overRowId: string) => void` | | Called on a row drag-and-drop reorder; the consumer reorders its data. | | `enableGrouping` | `boolean` | | Row grouping (group-by menu + drop-to-group zone + aggregated group rows). | | `enableExpanding` | `boolean` | | Row expansion (tree sub-rows and/or detail panels). Auto-on with grouping or when `renderDetailPanel`/`getSubRows` is provided. | | `enableStickyFooter` | `boolean` | | Pin the footer (aggregation/footer cells) to the bottom of the surface. Default false. | | `rowPinningDisplayMode` | `RowPinningDisplayMode` | | Where pinned rows show and how rows get pinned. | | `enableRowDragging` | `boolean` | | Whether rows have a drag handle for moving them out of the table. | | `onRowDragEnd` | `(props: { row: DataTableRow table: DataTableInstance over: RowDropTarget }) => void` | | Called when a row is dropped on another table or a drop zone in the same `DataTableDndProvider`. Move the row in your data. | | `enableTableHead` | `boolean` | | Whether the header rows render. | | `enableTableFooter` | `boolean` | | Whether the footer rows render (when columns define footers). | | `enableExpandAll` | `boolean` | | Whether the expand column's header toggles every row. | | `renderDetailPanel` | `(props: { row: DataTableRow table: DataTableInstance }) => React.ReactNode` | | Render an expanding detail panel for each row. | | `enableEditing` | `boolean` | | Whether editing is on (a per-row `enableEditing` function counts). | | `displayColumnDefOptions` | `\| Partial< Record>> > \| undefined` | | Overrides for the columns the table adds itself, by id. A new object re-renders the rows, so memoize it when you pass functions. | | `defaultDisplayColumn` | `Partial> \| undefined` | | Overrides for every column the table adds itself. | | `getCanEditRow` | `(row: DataTableRow) => boolean` | | Whether a row can be edited: the `enableEditing` function's answer, or `enableEditing` itself. | | `editDisplayMode` | `EditDisplayMode` | | How edits are surfaced. Default "cell". | | `createDisplayMode` | `CreateDisplayMode` | | How the create form is surfaced, independent of `editDisplayMode`: `"modal"` (dialog, default), `"row"` (an inline editable row at the top of the table), or `"custom"` (you render it from `isCreating` + `rowDraft`). | | `cancelEditing` | `() => void` | | Cancel as the user does: calls `onEditingRowCancel` / `onCreatingRowCancel`, then `cancelEdit`. | | `onEditingRowCancel` | `(props: { row: DataTableRow table: DataTableInstance }) => void` | | Called when the user cancels a row edit (Cancel, closing the dialog). | | `onCreatingRowCancel` | `(props: { table: DataTableInstance }) => void` | | Called when the user cancels the create form. | | `renderEditRowDialogContent` | `( props: EditDialogContentProps & { row: DataTableRow } ) => React.ReactNode` | | Replace the edit dialog's content. You get the built-in fields as `internalEditComponents`, plus `save()` / `cancel()`. | | `renderCreateRowDialogContent` | `( props: EditDialogContentProps ) => React.ReactNode` | | Replace the create dialog's content (same props, without `row`). | | `positionCreatingRow` | `"top" \| "bottom" \| number` | | Where the inline create row (`createDisplayMode: "row"`) appears: `"top"` (default), `"bottom"`, or a row index on the current page to insert it before (with row virtualization an index means `"top"`). | | `enableClickToCopy` | `ClickToCopyOption` | | Copy cells on click (`true`), from the cell menu (`"context-menu"`), or decide per cell with a function. Per-column override via meta. | | `enableCellActions` | `boolean \| ((cell: DataTableCell) => boolean)` | | A right-click menu on each cell with built-in Copy (when click-to-copy is on) and Edit (when the cell is editable) items, or a function deciding per cell. `renderCellActionMenuItems` customizes it. Default false. | | `onEditingCellSave` | `(props: { row: DataTableRow column: DataTableColumn value: unknown table: DataTableInstance }) => void` | | Called when a cell edit is committed (cell and table editing) with the new `value`. Persist it; the table does not change `data` itself. | | `onEditingRowSave` | `(props: { row: DataTableRow values: Record table: DataTableInstance exit: () => void }) => void` | | Called when a row edit is saved (row and modal editing) with the edited `values`. Call `exit()` to leave editing, e.g. after the save succeeds. | | `onCreatingRowSave` | `(props: { values: Record table: DataTableInstance exit: () => void }) => void` | | Called when the new-row editor is saved with the row's `values`. Call `exit()` to close it. | | `renderRowActions` | `(props: { row: DataTableRow table: DataTableInstance }) => React.ReactNode` | | Content of the row-actions column, replacing the default edit and menu buttons. | | `renderCellActionMenuItems` | `(props: { cell: DataTableCell row: DataTableRow table: DataTableInstance /** The built-in items (Copy, Edit) the menu shows for this cell. */ internalMenuItems: React.ReactNode closeMenu: () => void }) => React.ReactNode` | | The contents of the right-click menu on each cell (turns the menu on). Include `internalMenuItems` to keep the built-in Copy / Edit items. | | `renderRowActionMenuItems` | `(props: { row: DataTableRow table: DataTableInstance /** The built-in items (Edit, when the row can be edited). */ internalMenuItems: React.ReactNode closeMenu: () => void }) => React.ReactNode` | | Render a kebab menu in the row-actions column. Returns the menu items (e.g. ``); injects the actions column automatically. While editing is on, the Edit action moves into this menu: include `internalMenuItems` to keep it. | | `renderColumnActionsMenuItems` | `(props: { column: DataTableColumn table: DataTableInstance /** The built-in items (sort, filter, hide, group, pin). */ internalMenuItems: React.ReactNode closeMenu: () => void }) => React.ReactNode` | | The contents of every column-actions menu. Return your items (e.g. ``) with `internalMenuItems` where the built-in ones should go, or leave it out to replace them. | | `renderColumnFilterModeMenuItems` | `(props: { column: DataTableColumn modes: FilterMode[] currentMode: FilterMode onSelect: (mode: FilterMode) => void table: DataTableInstance }) => React.ReactNode` | | Replace the radio items in a column's filter-mode menu. Render your own items and call `onSelect(mode)` to switch; `modes` is the allowed set. | | `renderGlobalFilterModeMenuItems` | `(props: { modes: GlobalFilterMode[] currentMode: GlobalFilterMode onSelect: (mode: GlobalFilterMode) => void table: DataTableInstance }) => React.ReactNode` | | Replace the radio items in the global-search mode menu. Render your own items and call `onSelect(mode)` to switch; `modes` is the allowed set. | | `onRowClick` | `(props: RowEvent) => void` | | Fired when a body row is clicked / double-clicked. | | `onRowDoubleClick` | `(props: RowEvent) => void` | | Called when a row is double-clicked. Double-clicks on controls inside the row (edit, expand, pin…) are ignored. | | `onCellClick` | `(props: CellEvent) => void` | | Fired when a body cell is clicked / double-clicked. | | `onCellDoubleClick` | `(props: CellEvent) => void` | | Called when a cell is double-clicked. Double-clicks on controls inside the cell are ignored. | | `refs` | `DataTableRefs` | | DOM refs to the table's structural elements (populated after mount). | | `enableRowVirtualization` | `boolean` | | Virtualize body rows for large datasets (recommended past ~100 rows). The table surface becomes the scroll container — give it a bounded height via `className`/`style` (defaults to `max-h-[600px]`). Rows can still be dragged: the dragged row stays rendered while the drag scrolls. | | `enableColumnVirtualization` | `boolean` | | Virtualize columns for very wide tables. Applies fixed column widths; works with column pinning (pinned columns stay rendered), column ordering and grouped headers. | | `estimateRowHeight` | `number` | | Estimated row height (px) for the virtualizer. Default 52. | | `virtualOverscan` | `number` | | Extra rows rendered above/below the viewport. Default 8. | | `rowHeight` | `number` | | Flat height (px) applied to every row. | | `getRowHeight` | `(row: DataTableRow) => number \| "auto" \| null` | | Per-row height: a px number, `"auto"` (wrap + grow), or `null` for default. | | `rowVirtualizerOptions` | `RowVirtualizerOptions` | | Partial `@tanstack/react-virtual` options merged into the row virtualizer (overrides the built-in `count`/`estimateSize`/`overscan`/`measureElement`). A `rangeExtractor` picks the rendered rows; a row being dragged is added to its result. Accepts an object or a `({ table }) => options` function. | | `columnVirtualizerOptions` | `ColumnVirtualizerOptions` | | Partial `@tanstack/react-virtual` options merged into the column virtualizer. Accepts an object or a `({ table }) => options` function. | | `rowVirtualizerInstanceRef` | `React.RefObject` | | Ref populated with the row `Virtualizer` instance for imperative control (e.g. `scrollToIndex`). Only set when `enableRowVirtualization`. | | `columnVirtualizerInstanceRef` | `React.RefObject` | | Ref populated with the column `Virtualizer` instance. Only set when `enableColumnVirtualization`. | | `enableStickyHeader` | `boolean` | | Pin the header rows to the top of the scrollable surface. Also gives the surface a default viewport-bound max-height (`clamp(350px, 100dvh - 200px, 9999px)`) so tall content scrolls internally; override the bound with `slotProps.surface`. Default true. | | `enablePagination` | `boolean` | | Splits rows into pages with pagination controls. Default `true`; turn it off for virtualization or infinite scroll. | | `enableInfiniteScroll` | `boolean` | | Load more rows as the user scrolls near the bottom (auto-hides the pager). | | `onLoadMore` | `() => void` | | Called when the sentinel nears the viewport and more rows can be loaded. | | `hasNextPage` | `boolean` | | Consumer flag: more rows exist to load. | | `isFetchingNextPage` | `boolean` | | Consumer flag: a load is currently in flight. | | `infiniteScrollThreshold` | `number` | | Distance (px) from the bottom at which to prefetch. | | `positionPagination` | `"top" \| "bottom" \| "both" \| "none"` | | Where the pagination controls render. Default "bottom". "none" keeps pagination active but hides the controls. | | `paginationDisplayMode` | `PaginationDisplayMode` | | Pagination control style: `"default"` (range label + first/prev/next/last buttons), `"pages"` (numbered page buttons), or `"custom"` (render your own via `renderBottomToolbarCustomActions`). Default "default". | | `pageSizeOptions` | `number[]` | | Choices in the rows-per-page select. | | `slotProps` | `DataTableSlots \| undefined` | | Props for the table's parts; see the `slotProps` option. | | `columnFilterDisplayMode` | `ColumnFilterDisplayMode` | | Where column filter inputs live: `"subheader"` (a filter row under the header, default), `"popover"` (per-column popovers opened from the column header), or `"custom"` (you render them). | | `positionGlobalFilter` | `"left" \| "right" \| "none"` | | Which toolbar region the global search renders in. Default "right" (the internal-actions cluster). "left" places it next to the title/actions; "none" hides it (same as `enableGlobalFilter: false`). | | `positionToolbarAlertBanner` | `"top" \| "bottom" \| "head-overlay" \| "none"` | | Where the row-selection alert banner renders: above the table (`"top"`, default), below it (`"bottom"`), over the header row (`"head-overlay"`, with the select-all checkbox, so selecting rows does not move the table; needs the table head), or nowhere (`"none"`). | | `positionToolbarDropZone` | `"top" \| "bottom" \| "both" \| "none"` | | Where the group-by drop zone renders (grouping only). Default "top". | | `enableRowSelection` | `boolean` | | Whether rows can be selected. | | `selectAllMode` | `"page" \| "all"` | | Select-all scope for the header checkbox: the current page ("page", default) or every row ("all"). | | `enableSelectAll` | `boolean` | | Show the select-all checkbox in the selection column header. Default true. | | `enableBatchRowSelection` | `boolean` | | Shift-click a row's checkbox to select or clear the range since the last click. | | `enableTopToolbar` | `boolean` | | Shows the toolbar above the table (title, search, filters, view options). Default `true`. | | `enableBottomToolbar` | `boolean` | | Shows the toolbar below the table (pagination, custom actions). Default `true`. | | `enableDensityToggle` | `boolean` | | Show the density toggle in the toolbar. Default true. | | `enableFullscreenToggle` | `boolean` | | Show the full-screen toggle in the toolbar. Default true. | | `enableToolbarInternalActions` | `boolean` | | Show the toolbar's internal icon-action cluster (search, filters, column visibility, density, full screen). Default true. Hides the whole cluster at once; use the per-item flags for finer control. | | `enableKeyboardNavigation` | `boolean` | | Cell keyboard navigation with a single Tab stop: arrow keys, Home/End, PageUp/PageDown, Enter or F2 to activate a cell, Ctrl+C to copy it. Also makes the table an ARIA grid. Default `true`. | | `enableAnimations` | `boolean` | | Animate the table: rows glide when their order changes, fade in and out when they enter or leave the data, detail panels open and close smoothly, and the sort arrow, selection checkboxes and selected count transition. Always off when the user prefers reduced motion. Retime it with the CSS variables `--dt-motion-duration`, `--dt-motion-duration-fast` and `--dt-motion-ease` on the table. | | `title` | `React.ReactNode` | | Heading shown at the start of the top toolbar. | | `renderTopToolbarCustomActions` | `( props: DataTableSlotProps ) => React.ReactNode` | | Custom content rendered in the top toolbar's left region (next to the title), e.g. bulk-action buttons. | | `renderTopToolbar` | `(props: DataTableSlotProps) => React.ReactNode` | | Replace the entire top toolbar with custom content. | | `renderBottomToolbar` | `(props: DataTableSlotProps) => React.ReactNode` | | Replace the entire bottom toolbar (pagination region) with custom content. | | `renderToolbarInternalActions` | `( props: DataTableSlotProps ) => React.ReactNode` | | Replace the top toolbar's internal icon-action cluster with custom content. | | `renderBottomToolbarCustomActions` | `( props: DataTableSlotProps ) => React.ReactNode` | | Custom content rendered in the bottom toolbar's left region (next to pagination), e.g. summary text or actions. | | `renderCaption` | `(props: DataTableSlotProps) => React.ReactNode` | | Render a `` for the table (e.g. an accessible summary). | | `renderEmpty` | `(props: DataTableSlotProps) => React.ReactNode` | | Content shown when there are no rows. Defaults to `localization.noRecordsToDisplay`. | > **Everything else is TanStack:** All standard instance methods — `getState`, `getRowModel`, `getColumn`, > `setPageIndex`, `getFilteredRowModel`, `resetSorting`, … — are available > directly on `table` as usual. --- # Localization Every user-facing string flows through a localization table. Override any subset per instance via `useDataTable({ localization })`, or app-wide via [`DataTableConfigProvider`](/docs/guides/custom-icons). Function-valued entries handle interpolation and plurals. ```tsx const table = useDataTable({ data, columns, localization: { search: "Buscar", rowsPerPage: "Filas por página", paginationRange: (start, end, total) => `${start}–${end} de ${total}`, }, }) ``` See the [Localization guide](/docs/guides/localization) for a full example. The complete key set and English defaults: | Key | Type | Default | Description | | --- | --- | --- | --- | | `language` | `string` | `en` | BCP 47 tag used to format dates (e.g. "en", "de"). Numbers are formatted by the function keys of each localization, with that file's own tag. | | `selectAll` | `string` | `Select all` | | | `selectRow` | `string` | `Select row` | | | `clearSelection` | `string` | `Clear selection` | | | `rowsSelected` | `(selected: number, total: number) => string` | `(selected, total) => `${selected.toLocaleString("en")} of ${total.toLocaleString("en")} row${total === 1 ? "" : "s"} selected`` | | | `sortByColumnAsc` | `(column: string) => string` | `(column) => `Sort by ${column} ascending`` | | | `sortByColumnDesc` | `(column: string) => string` | `(column) => `Sort by ${column} descending`` | | | `sortAscending` | `string` | `Sort ascending` | | | `sortDescending` | `string` | `Sort descending` | | | `clearSort` | `string` | `Clear sort` | | | `sortedAscending` | `string` | `Sorted ascending` | | | `sortedDescending` | `string` | `Sorted descending` | | | `columnActions` | `string` | `Column actions` | | | `hideColumn` | `string` | `Hide column` | | | `showAllColumns` | `string` | `Show all columns` | | | `pinToLeft` | `string` | `Pin to left` | | | `pinToRight` | `string` | `Pin to right` | | | `unpin` | `string` | `Unpin` | | | `reorderColumn` | `string` | `Reorder column` | | | `reorderRow` | `string` | `Reorder row` | | | `pinRow` | `string` | `Pin row` | | | `unpinRow` | `string` | `Unpin row` | | | `resizeColumn` | `string` | `Resize column` | | | `groupByColumn` | `(column: string) => string` | `(column) => `Group by ${column}`` | | | `ungroupByColumn` | `(column: string) => string` | `(column) => `Ungroup by ${column}`` | | | `groupedBy` | `string` | `Grouped by` | | | `dropToGroupBy` | `string` | `Drag a column here to group by it` | | | `expand` | `string` | `Expand` | | | `collapse` | `string` | `Collapse` | | | `expandAll` | `string` | `Expand all` | | | `collapseAll` | `string` | `Collapse all` | | | `toggleRowExpanded` | `string` | `Toggle row expanded` | | | `columnVisibility` | `string` | `Column visibility` | | | `toggleColumnVisibility` | `string` | `Toggle column visibility` | | | `filterByColumn` | `(column: string) => string` | `(column) => `Filter by ${column}`` | | | `clearFilter` | `string` | `Clear filter` | | | `filterMode` | `string` | `Filter mode` | | | `changeFilterMode` | `string` | `Change filter mode` | | | `filterPlaceholder` | `(column: string) => string` | `(column) => `Filter ${column}…`` | | | `facetFilterAny` | `string` | `Any` | The choice that clears a toolbar facet filter. | | `resetFilters` | `string` | `Reset` | The toolbar button that clears every column filter. | | `showColumnFilters` | `string` | `Show filters` | | | `hideColumnFilters` | `string` | `Hide filters` | | | `min` | `string` | `Min` | | | `max` | `string` | `Max` | | | `pickDate` | `string` | `Pick a date` | | | `pickDateRange` | `string` | `Pick a date range` | | | `true` | `string` | `True` | | | `false` | `string` | `False` | | | `filterModes` | `Record` | `{ fuzzy: "Fuzzy", contains: "Contains", startsWith: "Starts with", endsWith: "Ends with", equals: "Equals", notEquals: "Not equals", empty: "Empty", notEmpty: "Not empty", between: "Between (exclusive)", betweenInclusive: "Between (inclusive)", greaterThan: "Greater than", greaterThanOrEqualTo: "Greater than or equal to", lessThan: "Less than", lessThanOrEqualTo: "Less than or equal to", before: "Before", after: "After", betweenDates: "Between", equalsString: "Equals", arrIncludesSome: "Includes", equalsBool: "Equals", }` | Labels for each filter mode, keyed by the `FilterMode` string. | | `advancedFilters` | `string` | `Advanced filters` | | | `advancedFiltersMatch` | `(logic: string) => string` | `(logic) => `Match ${logic} of the following rules`` | The "Match All/Any of the following rules" sentence; `logic` marks where the All/Any picker goes, so each language can order the words. | | `advancedFiltersMatchAll` | `string` | `All` | | | `advancedFiltersMatchAny` | `string` | `Any` | | | `advancedFiltersAddRule` | `string` | `Add filter` | | | `advancedFiltersApply` | `string` | `Apply` | | | `advancedFiltersClearAll` | `string` | `Clear all` | | | `advancedFiltersColumn` | `string` | `Column` | | | `advancedFiltersOperator` | `string` | `Operator` | | | `advancedFiltersValue` | `string` | `Value` | | | `advancedFiltersEmpty` | `string` | `No filters yet. Add one to get started.` | | | `removeFilterRule` | `string` | `Remove filter` | | | `advancedFilterOperators` | `Record` | `{ contains: "contains", notContains: "does not contain", startsWith: "starts with", endsWith: "ends with", equals: "equals", notEquals: "does not equal", isEmpty: "is empty", isNotEmpty: "is not empty", greaterThan: "greater than", greaterThanOrEqual: "greater than or equal", lessThan: "less than", lessThanOrEqual: "less than or equal", between: "is between", }` | Operator labels, keyed by `AdvancedFilterOperator`. | | `search` | `string` | `Search` | | | `searchPlaceholder` | `string` | `Search…` | | | `clearSearch` | `string` | `Clear search` | | | `globalFilterMode` | `string` | `Search mode` | | | `toggleDensity` | `string` | `Toggle density` | | | `densityComfortable` | `string` | `Comfortable` | | | `densityCompact` | `string` | `Compact` | | | `densitySpacious` | `string` | `Spacious` | | | `enterFullscreen` | `string` | `Enter full screen` | | | `exitFullscreen` | `string` | `Exit full screen` | | | `rowsPerPage` | `string` | `Rows per page` | | | `paginationRange` | `(start: number, end: number, total: number) => string` | `(start, end, total) => `${start.toLocaleString("en")}–${end.toLocaleString("en")} of ${total.toLocaleString("en")}`` | | | `goToFirstPage` | `string` | `Go to first page` | | | `goToPreviousPage` | `string` | `Go to previous page` | | | `goToNextPage` | `string` | `Go to next page` | | | `goToLastPage` | `string` | `Go to last page` | | | `goToPage` | `(page: number) => string` | `(page) => `Go to page ${page.toLocaleString("en")}`` | | | `rowActions` | `string` | `Row actions` | | | `edit` | `string` | `Edit` | | | `save` | `string` | `Save` | | | `cancel` | `string` | `Cancel` | | | `delete` | `string` | `Delete` | | | `create` | `string` | `Create` | | | `createNewRow` | `string` | `Create new row` | | | `editRow` | `string` | `Edit row` | | | `required` | `string` | `Required` | | | `copy` | `string` | `Copy` | | | `copied` | `string` | `Copied` | | | `cellActions` | `string` | `Cell actions` | | | `noRecordsToDisplay` | `string` | `No records to display` | | | `noResultsFound` | `string` | `No results found` | Shown instead of `noRecordsToDisplay` when filters or the search hide every row. | | `aggregationFns` | `Record` | | Labels for aggregation function names, shown when a column lists several (`aggregationFn: ["sum", "mean"]`). Merged with the English defaults by key; unknown names show as written. | | `loading` | `string` | `Loading…` | | | `loadingMore` | `string` | `Loading more…` | | | `dragStart` | `(label: string) => string` | `(label) => `Picked up ${label}.`` | | | `dragOver` | `(label: string, over: string) => string` | `(label, over) => `${label} is over ${over}.`` | | | `dragEnd` | `(label: string, over: string) => string` | `(label, over) => `${label} was dropped on ${over}.`` | | | `dragCancel` | `(label: string) => string` | `(label) => `Dragging was cancelled. ${label} was dropped.`` | | | `dragInstructions` | `string` | `To pick up an item, press space or enter. While dragging, use the arrow keys to move it. Press space or enter again to drop it, or press escape to cancel.` | | | `dragRowLabel` | `(position: number) => string` | `(position) => `Row ${position.toLocaleString("en")}`` | | --- # Icons Every glyph the table renders is a named slot. Override any subset via `useDataTable({ icons })` (or app-wide via [`DataTableConfigProvider`](/docs/guides/custom-icons)) to swap icon libraries or individual icons. Each value is an `IconComponent` — any component accepting a `className`. ```tsx import { ArrowUp, ArrowDown, MoreVertical } from "lucide-react" const table = useDataTable({ data, columns, icons: { sortAscending: ArrowUp, sortDescending: ArrowDown, columnActions: MoreVertical, }, }) ``` See the [Custom icons guide](/docs/guides/custom-icons) for swapping the whole set. The complete slot list and Remix Icon defaults: | Slot | Type | Default | Description | | --- | --- | --- | --- | | `sortAscending` | `IconComponent` | `ArrowUp` | Header sort indicator: sorted ascending. | | `sortDescending` | `IconComponent` | `ArrowDown` | Header sort indicator: sorted descending. | | `sortUnsorted` | `IconComponent` | `ChevronsUpDown` | Header sort indicator: sortable but not sorted. | | `columnActions` | `IconComponent` | `MoreVertical` | Column-actions menu trigger in each header. | | `filter` | `IconComponent` | `Filter` | Filter toggle and filter menu items. | | `filterOff` | `IconComponent` | `FilterX` | Hide-filters state of the filter toggle and clear-filter items. | | `advancedFilter` | `IconComponent` | `ListFilter` | Advanced filter panel toggle. | | `clearAll` | `IconComponent` | `RotateCcw` | Show-all-columns reset action in the column menu. | | `clearSort` | `IconComponent` | `ListX` | Clear-sort item in the column menu. When not overridden but `clearAll` is, it uses your `clearAll`. | | `clearFilter` | `IconComponent` | `FilterX` | Clear-filter item in the column menu. When not overridden but `filterOff` is, it uses your `filterOff`. | | `hide` | `IconComponent` | `EyeOff` | Hide-column menu item. | | `pin` | `IconComponent` | `Pin` | Generic pin glyph. Any of `pinLeft`, `pinRight`, `pinTop` and `pinBottom` you don't override falls back to this one, unrotated. | | `pinLeft` | `IconComponent` | `PinLeft` | Pin-column-to-the-left menu item. The default needle points left. | | `pinRight` | `IconComponent` | `PinRight` | Pin-column-to-the-right menu item. The default needle points right. | | `pinTop` | `IconComponent` | `Pin` | Pin-row-to-the-top button while the row is unpinned. The default is the plain pin. | | `pinBottom` | `IconComponent` | `Pin` | Pin-row-to-the-bottom button while the row is unpinned (used when `rowPinningDisplayMode` is `"bottom"`). The default is the plain pin. | | `pinnedRow` | `IconComponent` | `Pin` | Shown on a pinned row's pin button. | | `unpin` | `IconComponent` | `PinOff` | Unpin column and unpin row actions. | | `group` | `IconComponent` | `Group` | Group-by menu item and grouping chips. | | `columnVisibility` | `IconComponent` | `Columns3` | Column-visibility (show/hide columns) toolbar button. | | `density` | `IconComponent` | `Rows3` | Density toggle toolbar button. | | `fullscreenEnter` | `IconComponent` | `Maximize2` | Full-screen toggle while not full screen. | | `fullscreenExit` | `IconComponent` | `Minimize2` | Full-screen toggle while full screen. | | `search` | `IconComponent` | `Search` | Global search button and search-mode menu. | | `clear` | `IconComponent` | `X` | Clear buttons (search box, filter inputs, grouping chips). | | `pageFirst` | `IconComponent` | `ChevronsLeft` | Pagination: first page. Mirrored in right-to-left tables. | | `pagePrev` | `IconComponent` | `ChevronLeft` | Pagination: previous page. Mirrored in right-to-left tables. | | `pageNext` | `IconComponent` | `ChevronRight` | Pagination: next page. Mirrored in right-to-left tables. | | `pageLast` | `IconComponent` | `ChevronsRight` | Pagination: last page. Mirrored in right-to-left tables. | | `expanded` | `IconComponent` | `ChevronDown` | Expand control of an expanded row or group. | | `collapsed` | `IconComponent` | `ChevronRight` | Expand control of a collapsed row or group. Mirrored in right-to-left tables. | | `dragHandle` | `IconComponent` | `GripVertical` | Drag grip for column and row reordering. | | `edit` | `IconComponent` | `Pencil` | Edit row / edit cell action. | | `copy` | `IconComponent` | `Copy` | Copy action in the cell menu. | | `save` | `IconComponent` | `Check` | Save action while editing a row. | | `cancel` | `IconComponent` | `X` | Cancel action while editing a row. | | `calendar` | `IconComponent` | `Calendar` | Date filter field trigger. |