# 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 }) => (
)
: 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 (
)
}
```
## 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 }) => (
>
),
})
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` | `