Reusable React data table components built on TanStack Table with multiple UI adapter entrypoints.
data-table-pro ships one shared table implementation with base and
virtualization-first adapter entrypoints:
data-table-pro: shadcn-compatible defaultdata-table-pro/heroui: HeroUI-flavored adapterdata-table-pro/thegridcn: The Gridcn-flavored adapterdata-table-pro/virtual: shadcn with eager TanStack Virtual supportdata-table-pro/heroui/virtual: HeroUI with eager TanStack Virtual supportdata-table-pro/thegridcn/virtual: The Gridcn with eager TanStack Virtual support
All adapter entrypoints export the same table runtime API:
DataTable
Dedicated subpath exports are also available:
data-table-pro/url-state:useDataTableUrlStatedata-table-pro/data-source: typed server data source hook with manual table propsdata-table-pro/adapter: stable non-virtual adapter-authoring factorydata-table-pro/adapter/virtual: virtual adapter-authoring factorydata-table-pro/advanced: advanced composition hooks, panels, and adapter helpersdata-table-pro/types: public TypeScript types
The current feature set includes client/manual filtering with local or
server-provided facets, grouping and aggregation, sorting and pagination,
unknown-total and automatic page sizing, row and cell-range selection,
interactive-grid keyboard navigation, detail panels, table/card views,
row/card virtualization, nested column groups, column
sizing/order/pinning/visibility, versioned persistence, named saved views,
versioned URL state, CSV and clipboard export, inline row editing, density
controls, loading/empty/error states, summary rows, alternate row shading,
infinite loading, typed server data sources, and host-owned drag/upload
integrations. See
docs/API.md for the complete contract.
- package output is ESM-only;
require()/CommonJS entrypoints were removed - React peers are now
react@^19.2.0andreact-dom@^19.2.0 nuqsis an optional peer used only bydata-table-pro/url-state- toolbar search now filters client-side tables by default; disable with
manualFilteringorenableToolbarQueryFiltering={false} - column filters, CSV export, row expansion, density, column pinning/reordering, selection policies, labels, and column preference persistence were added
Version 4.0.0 removes no public prop, type, or package entrypoint. It is a compatibility-first major that releases the accumulated state, persistence, quality, dependency, accessibility, and package-splitting work after 3.0.9. The previously planned API cleanup is deferred to a future 5.0 release.
Version 4.4.0 is an additive minor release. Native table semantics and the existing toolbar remain the defaults; grouping, interactive-grid navigation, cell ranges, clipboard/paste, enhanced data operations, auto page sizing, print/fullscreen controls, and error overlays are individually opt-in. No public prop, type, or entrypoint was removed.
pnpm add github:Dastari/data-table-pro#v4.4.0This package is installed from GitHub refs. It is not published to npm.
Release tags such as v4.4.0 include committed dist/ output, so consumers
do not need to allow package build scripts during install.
Peer dependencies:
react@^19.2.8react-dom@^19.2.8nuqs@^2.9.2only when usingdata-table-pro/url-state@heroui/styles@^3.2.2when usingdata-table-pro/heroui
Baseline assumptions:
- Tailwind CSS v4
- host application owns theme tokens
- host application imports
data-table-pro/styles.css
import { DataTable, type DataTableColumnDef } from "data-table-pro";
import { useDataTableUrlState } from "data-table-pro/url-state";
import "data-table-pro/styles.css";import { DataTable, type DataTableColumnDef } from "data-table-pro/heroui";
import "@heroui/styles";
import "data-table-pro/styles.css";import { DataTable, type DataTableColumnDef } from "data-table-pro/thegridcn";
import "data-table-pro/styles.css";
import "./thegridcn-theme.css";Use a /virtual entrypoint when virtualization is enabled on initial render
or when the application preloads routes:
import { DataTable } from "data-table-pro/virtual";
<DataTable
columns={columns}
data={rows}
getRowId={(row) => row.id}
virtualization
/>;The existing base entrypoints remain source-compatible with
virtualization. They load the virtual row/card implementation only when the
prop enables it and render a bounded initial set as the Suspense fallback
(20 rows or 12 cards by default). Applications that never enable
virtualization do not statically load @tanstack/react-virtual.
Custom adapter authors should use createDataTable from
data-table-pro/adapter, or createVirtualDataTable from
data-table-pro/adapter/virtual.
data-table-pro/advanced is the supported import path for adapter authors and consumers composing around the extracted internals:
import {
createDataTable,
primitiveUiKit,
useColumnLayout,
useControllableState,
useDataTableColumns,
useDataTableInstance,
useDataTableState,
useRowEditing,
useStableCallback,
DataTableBodyRow,
DataTableCardPanel,
DataTableFooterSection,
DataTableHeaderCell,
DataTableTablePanel,
DataTableToolbarSection,
type DataTableUiKit,
} from "data-table-pro/advanced";Use this subpath instead of deep imports from src, dist/chunk-*, or other generated files. The regular adapter entrypoints remain the recommended API for application tables.
Always import:
@import "tailwindcss";
@import "data-table-pro/styles.css";data-table-pro/styles.css provides:
- Tailwind package scanning for built JavaScript output
- package-owned inline-size container queries used by column
hideOnand the toolbar
Responsive behavior is based on the table's allocated width, not the browser
viewport. The package uses fixed sm/md/lg/xl/2xl thresholds of
640/768/1024/1280/1536px for both CSS presentation and TanStack column state;
host Tailwind breakpoint customization and root font-size changes do not alter
these thresholds.
It does not provide:
- shadcn theme tokens
- HeroUI theme tokens
- The Gridcn theme tokens
DataTable defaults to flexGrow={true} and is designed to fill the remaining height of a constrained flex region. Consumers should only need to place it inside a normal flex-1 min-h-0 content area:
<main className="flex h-full min-h-0 flex-col">
<section className="flex min-h-0 flex-1 flex-col">
<DataTable flexGrow />
</section>
</main>The package owns the internal flex chain between root, toolbar, table/card viewport, scroll area, and footer. Toolbar and footer areas are shrink-0; the central table/card viewport and scroll area are min-h-0 flex-1.
Remaining parent requirement:
- the nearest height-constrained parent must provide a real flex sizing boundary such as
h-full,h-screen, or a fixed-height panel plusmin-h-0
Card mode supports three built-in sizing modes:
<DataTable
viewMode="card"
cardRenderer={renderCard}
cardSizing="content"
/>cardSizing="fixed"is the default. It uses start-aligned grid tracks capped at18rem.cardSizing="content"usesflexandflex-wrap; card items arew-fit max-w-full, so renderer content determines each card width.cardSizing="fluid"uses responsive1frgrid tracks and stretches card items/renderers to the track width.
Use cardGridClassName and cardClassName as low-level overrides only when the built-in sizing modes are not enough.
Card mode can virtualize large card sets:
<DataTable
viewMode="card"
cardRenderer={renderCard}
virtualization={{
card: {
enabled: true,
estimateCardHeight: 280,
fallbackCardCount: 12,
overscan: 4,
lanes: "auto",
},
}}
/>virtualization.card.lanes accepts a positive number or "auto". "auto" derives lanes from the card viewport width. Before the scroll viewport is measurable, fallbackCardCount bounds first-paint work so large card collections do not mount in full.
Virtual rows are measured after mount, including variable-height wrapped table
cells and card lanes. Keep getRowId, row objects, and column definitions
stable and immutable: row ids are the virtual scroll keys and built-in search
indexes values by row/column identity. Development builds warn about duplicate
row ids and common identity churn. See the benchmark harness
for repeatable row- and column-scale measurements. Column virtualization is not
currently offered because it would compromise the native table's grouped-header,
pinned-column, resize, detail-row, and accessibility contracts.
Use this if the host app already provides shadcn-compatible theme tokens and styles.
The host app must also import:
@import "@heroui/styles";Use this adapter in apps standardized on HeroUI, React 19, and Tailwind v4.
Install @heroui/styles in the host app. This package does not bundle HeroUI styles for consumers.
The host app must provide a The Gridcn theme or token file. This package does not run the The Gridcn registry installer for the consumer.
import { DataTable, type DataTableColumnDef } from "data-table-pro";
type Person = {
id: string;
name: string;
email: string;
};
const columns: Array<DataTableColumnDef<Person>> = [
{
accessorKey: "name",
header: "Name",
},
{
accessorKey: "email",
header: "Email",
},
];
export function PeopleTable({ rows }: { rows: Array<Person> }) {
return (
<DataTable
columns={columns}
data={rows}
getRowId={(row) => row.id}
title="People"
/>
);
}Define a group with an id, a header, and nested columns. Groups can be
nested to any depth and use the same TanStack-compatible definition shape as
leaf columns:
import type {
DataTableColumnDef,
DataTableColumnGroupDef,
} from "data-table-pro";
const contactGroup: DataTableColumnGroupDef<Person> = {
id: "contact",
header: "Contact",
meta: {
align: "center",
headerClassName: "bg-muted/50 font-semibold",
},
columns: [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "email", header: "Email" },
],
};
const columns: Array<DataTableColumnDef<Person>> = [contactGroup];The table creates one header row per nesting level and gives every group
header the span of its visible leaf columns. header may be a string or a
TanStack header render function. Use meta.headerClassName,
meta.headerStyle, and meta.align for group styling, or the group-specific
headerClassName, headerStyle, and headerHeight fields. description
provides an accessible description and native tooltip for the group heading.
Set columnGroupHeaderHeight to establish a table-wide group-heading height;
a group's headerHeight takes precedence.
Visibility, filters, ordering, pinning, sizing, editing, responsive hiding,
and CSV export continue to operate on leaf columns. Groups preserve their
shared headings during drag and keyboard reordering: by default a leaf may move
only within the same nested group. Set freeReordering: true on every group
boundary that a leaf should be able to cross (both groups when moving between
groups) to permit splitting or joining groups. A group resize handle resizes its
visible descendants proportionally.
<DataTable
columns={columns}
data={rows}
getRowId={(row) => row.id}
toolbarQueryValue={query}
onToolbarQueryValueChange={setQuery}
/>Enable first-class row pinning to keep important records at the top or bottom
of the table. rowPinning and onRowPinningChange follow TanStack's
{ top: string[]; bottom: string[] } state shape and also participate in
initialState, unified state, persistence, saved views, and apiRef.
const apiRef = React.createRef<DataTableApi<Person>>();
<DataTable
apiRef={apiRef}
columns={columns}
data={rows}
getRowId={(row) => row.id}
enableRowPinning
initialState={{ rowPinning: { top: ["important-id"], bottom: [] } }}
/>;
apiRef.current?.pinRow("important-id", "top");
apiRef.current?.unpinRow("important-id");When enabled, each row's action menu includes top, bottom, and unpin actions.
Pinned rows render outside the virtualized center list and receive
data-dtp-slot="data-table-pinned-row" plus data-row-pinned="top" or
"bottom"; use classNames.rowPinnedTop and classNames.rowPinnedBottom
for adapter styling. keepPinnedRows defaults to true, keeping supplied rows
visible after client filtering or pagination; set it to false to hide pinned
rows that are not in the current model. For server/manual pagination, the
caller must still provide a pinned row in data for it to render. Card view
preserves the ordinary card order; it does not provide separate top/bottom
pinned regions.
Toolbar search filters local/client-side data by default. Server-side tables can keep filtering consumer-owned:
<DataTable
columns={columns}
data={rows}
getRowId={(row) => row.id}
manualFiltering
toolbarQueryValue={query}
onToolbarQueryValueChange={setQuery}
/>Client-side toolbar search now runs through TanStack Table's global-filter
pipeline. This means globalFilterFn, nested leaf columns, accessor functions,
sorting, filtering, pagination, and CSV export share one consistent row model.
Unknown-total server pagination can keep Next/Previous navigation without inventing a total:
<DataTable
columns={columns}
data={pageRows}
getRowId={(row) => row.id}
manualPagination
pageIndex={pageIndex}
pageSize={pageSize}
hasNextPage={hasNextPage}
onPageIndexChange={setPageIndex}
/>CSV export supports "filtered", "page", "selected", and "all" scopes.
String values that begin with spreadsheet formula characters are neutralized
by default; trusted applications can set escapeFormulaValues: false.
Versioned state persistence is available through the additive persistence
configuration:
<DataTable
columns={columns}
data={rows}
getRowId={(row) => row.id}
persistence={{
key: "people-table",
version: 2,
slices: ["visibility", "sizing", "order", "pinning", "density"],
debounceMs: 150,
migrate: (payload) => migratePeopleTableState(payload),
onError: ({ error, operation }) => {
reportPersistenceError(operation, error);
},
}}
/>columnPrefsKey="people-table" remains supported as a shorthand. Legacy raw
preference objects are validated, loaded, and upgraded to the versioned
envelope on the next write.
Unified state and imperative commands can be adopted incrementally:
const apiRef = React.createRef<DataTableApi<Person>>();
const [tableState, setTableState] = React.useState<DataTableState>(
initialPeopleTableState,
);
<DataTable
apiRef={apiRef}
columns={columns}
data={rows}
getRowId={(row) => row.id}
state={tableState}
onStateChange={setTableState}
/>;
apiRef.current?.scrollToRow("person-42");
const snapshot = apiRef.current?.snapshot();Legacy controlled props remain supported and take precedence over the matching unified slice during the 4.x compatibility window. A development warning identifies conflicting inputs.
Enhanced URL state remains opt-in and isolated from the base bundle:
const url = useDataTableUrlState({
keyPrefix: "people-",
version: 2,
enabled: [
"columnFilters",
"columnVisibility",
"density",
"columnOrder",
"columnPinning",
],
migrate: (payload, targetVersion) =>
migratePeopleUrlState(payload, targetVersion),
});Grouping and row selection are also available as URL slices, but selection is
never written unless "rowSelection" is explicitly enabled.
Named saved views use versioned storage. Applications can keep their own UI, or opt into the built-in table-options controls:
<DataTable
apiRef={apiRef}
columns={columns}
data={rows}
getRowId={(row) => row.id}
savedViews={{
key: "people-table",
version: 1,
onChange: (views) => setSavedViews(views),
}}
toolbarDataOperations={{
columnChooser: true,
resetLayout: true,
savedViews: true,
}}
/>;
const saved = apiRef.current?.createSavedView("My view");
apiRef.current?.applySavedView(saved?.id ?? "");
apiRef.current?.resetState({ clearPersistence: true });The enhanced column chooser is searchable and includes bulk show/hide,
keyboard-accessible move-earlier/move-later actions, and pin controls when
enableColumnPinning is enabled. Active search and column filters are also
shown as removable chips.
Declare toolbar filters on column meta:
const columns: Array<DataTableColumnDef<Person>> = [
{
accessorKey: "status",
header: "Status",
meta: {
filter: {
type: "multi",
options: ["active", "paused", "archived"],
},
},
},
];Use type: "faceted" for a searchable multi-select filter with local
TanStack option counts. Supply faceting.options when counts come from the
server. Grouping is controlled with grouping / onGroupingChange (or stored
in initialState.grouping); set enableGrouping to expose the accessible
group/ungroup menu and removable grouping bar.
const columns: Array<DataTableColumnDef<Person>> = [
{
accessorKey: "team",
header: "Team",
enableGrouping: true,
meta: { filter: { type: "faceted" } },
},
{
accessorKey: "hours",
header: "Hours",
aggregationFn: "sum",
},
];
<DataTable
columns={columns}
data={people}
getRowId={(person) => person.id}
enableGrouping
initialState={{ grouping: ["team"] }}
/>;Keep the default native table for reading/browsing. Opt into grid mode for a keyboard-operated data workspace, then enable rectangular selection and clipboard handling independently:
<DataTable
columns={columns}
data={rows}
getRowId={(row) => row.id}
interactiveGrid
enableCellSelection
clipboard={{
copy: true,
paste: {
onPaste: ({ values }) => applyPastedValues(values),
},
}}
toolbarDataOperations
autoPageSize
enablePrint
enableFullscreen
stateOverlay={{ error, onRetry: reload }}
/>Grid mode provides roving cell focus and arrow/Home/End/Page navigation. Shift extends the cell range, Ctrl/Cmd+C copies the range as formula-safe TSV, and optional undo/redo callbacks remain application-owned. Enhanced toolbar, clipboard, auto-size, and error-overlay implementations are first-use chunks.
Run the bundled demo workbench:
pnpm demoThe demo uses generated employee data and can switch between the shadcn, HeroUI, and The Gridcn adapters in light or dark themes. It exercises selection, sorting, local automatic pagination, optional table/card virtualization, column resizing, search, custom filter rows inside the table toolbar, custom toolbar controls, row actions, selection actions, inline editing, card view, hidden rows, infinite scroll, file upload hooks, drag hooks, and loading states.
Contributors need a jsdom 30-supported Node.js release (^22.22.2,
^24.15.0, or >=26) and pnpm 11.17.0. CI uses Node.js 22.22.2.
TypeScript 7 provides the tsc command; the typescript package name
intentionally points to the official TypeScript 6 compatibility package
because ESLint and declaration bundling still require the compiler API that
TypeScript 7.0 does not ship.
Run the same focused checks used by CI:
pnpm lint
pnpm typecheck
pnpm demo:typecheck
pnpm test:coverage
pnpm build
pnpm api:check
pnpm test:consumer
pnpm demo:build
pnpm bundle:check
pnpm test:browsertest:browser covers shadcn, HeroUI, and The Gridcn in light and dark themes
with layout assertions, axe audits, and screenshot baselines.
test:consumer packs the repository and builds a clean consumer fixture
against the resulting tarball, including every supported adapter and subpath.
bundle:check enforces the base/adapter/URL-state/data-source/demo gzip budgets
and fails if a base, stable adapter-authoring, or data-source entrypoint
statically reaches TanStack Virtual.
api-snapshots/public-api.md is the generated, reviewable declaration
reference for this TypeScript package; the repository does not contain a Rust
crate or Rustdoc output. When a reviewed public declaration change is
intentional, regenerate the snapshot with
pnpm api:update. Install the browser once with
pnpm exec playwright install chromium before running browser tests locally.
Toolbar query note:
toolbarQueryValueandonToolbarQueryValueChangecontrol the toolbar search input only- toolbar query and
column.meta.filtercontrols filter client-side rows by default, including text, option, boolean, numeric-range, and date-range filters - use
manualFilteringfor server-side filtering
Cell overflow note:
- primitive table cells now truncate with ellipsis by default
- custom rendered table cells now clip to their cell bounds by default
- override per column with
column.meta.overflow: "truncate" | "clip" | "wrap" | "visible" - use
"visible"for non-portaled overlay-style cell content when intentional visual overflow is required
Mobile toolbar note:
- built-in toolbar controls compact automatically in narrow container widths
- use
compactToolbarto supply icon-only custom filter/action content for the collapsed toolbar strip compactToolbarstays inline through small and medium container widthscustomToolbarbecomes the separate desktop secondary toolbar row from the large container breakpoint upward- if
compactToolbaris omitted,customToolbaris reused in the compact row as a fallback
Example:
<DataTable
columns={columns}
data={rows}
getRowId={(row) => row.id}
toolbarQueryValue={query}
onToolbarQueryValueChange={setQuery}
customToolbar={
<>
<Button type="button" variant="outline">
Status
</Button>
<Button type="button" variant="outline">
Reset Filters
</Button>
</>
}
compactToolbar={
<>
<Button type="button" variant="outline" size="icon-sm" aria-label="Status filters">
<IconFilter />
</Button>
<Button type="button" variant="outline" size="icon-sm" aria-label="Reset filters">
<IconRefresh />
</Button>
</>
}
/>Use compactToolbar when the desktop toolbar content is too wide or too text-heavy for the collapsed small/medium toolbar strip.
- Existing shadcn consumers can keep importing from
data-table-pro. - Existing consumers should now import
data-table-pro/styles.css. - Existing consumers can remove copied
.data-table-container-queryand.dt-hide-on-*helpers from app globals after upgrading. - Existing consumers must import
useDataTableUrlStatefromdata-table-pro/url-state. - Existing consumers must rename legacy
search*props totoolbarQuery*. - HeroUI migrations change imports to
data-table-pro/herouiand add@heroui/styles. - The Gridcn migrations change imports to
data-table-pro/thegridcnand add a host-supplied The Gridcn theme/token stylesheet.
See: