ComponentsDataTable
DataTable
Display rows of data with sorting, filtering, selection, and pagination.
On this page
Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.
View registry files- TanStack Table
(@tanstack/react-table)Headless table state and row models for sorting, filtering, pagination, selection, and column visibility. Dethink supplies the table markup and styling.
Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.
Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.
Import the component into your page or component file.
import {
DataTable,
type DataTableColumnDef,
} from "@dethink/components";Try the examples, then open the code to use them in your app. Headers are real buttons — sort with Enter or Space.
Sorting and filtering
Column defs with a custom status cell, default sort, and the global filter toolbar.
| api-gateway | production | success | 4m 12s |
| api-gateway | staging | success | 3m 58s |
| billing | production | failed | 1m 03s |
| web-app | staging | success | 6m 41s |
| worker | production | running | — |
Show sourceexamples/data-table/basic.tsx
"use client";
import { DataTable, type DataTableColumnDef } from "@dethink/components";
type Deploy = {
id: string;
service: string;
env: string;
status: "success" | "failed" | "running";
duration: string;
};
const deploys: Deploy[] = [
{
id: "d1",
service: "api-gateway",
env: "production",
status: "success",
duration: "4m 12s",
},
{
id: "d2",
service: "billing",
env: "production",
status: "failed",
duration: "1m 03s",
},
{
id: "d3",
service: "web-app",
env: "staging",
status: "success",
duration: "6m 41s",
},
{
id: "d4",
service: "worker",
env: "production",
status: "running",
duration: "—",
},
{
id: "d5",
service: "api-gateway",
env: "staging",
status: "success",
duration: "3m 58s",
},
];
const columns: DataTableColumnDef<Deploy>[] = [
{ accessorKey: "service", header: "Service" },
{ accessorKey: "env", header: "Environment" },
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const status = row.original.status;
const tone =
status === "success"
? "text-success"
: status === "failed"
? "text-destructive"
: "text-info";
return <span className={`font-medium ${tone}`}>{status}</span>;
},
},
{ accessorKey: "duration", header: "Duration" },
];
export function DataTableBasic() {
return (
<DataTable
columns={columns}
data={deploys}
getRowId={(row) => row.id}
caption="Latest deployments"
enableGlobalFilter
globalFilterPlaceholder="Filter deployments…"
defaultSorting={[{ id: "service", desc: false }]}
/>
);
}Empty, loading, and error states
The three non-data states render inside the table region and announce to assistive tech.
| No projects yet. Create one to get started. |
Show sourceexamples/data-table/states.tsx
"use client";
import { useState } from "react";
import {
Button,
DataTable,
type DataTableColumnDef,
} from "@dethink/components";
type Row = { id: string; name: string };
const columns: DataTableColumnDef<Row>[] = [
{ accessorKey: "name", header: "Name" },
];
export function DataTableStates() {
const [state, setState] = useState<"loading" | "empty" | "error">("empty");
return (
<div className="space-y-3">
<div className="flex justify-center gap-2">
{(["empty", "loading", "error"] as const).map((mode) => (
<Button
key={mode}
size="sm"
variant={state === mode ? "solid" : "outline"}
onClick={() => setState(mode)}
>
{mode}
</Button>
))}
</div>
<DataTable
columns={columns}
data={[]}
getRowId={(row) => row.id}
loading={state === "loading"}
error={
state === "error"
? "Could not load projects — retry shortly."
: undefined
}
emptyContent="No projects yet. Create one to get started."
/>
</div>
);
}Examples that combine components for common tasks.
Incident dashboard with bulk actions
Sorting, filtering, multi-select, and pagination working together: select incidents across the filtered set, acknowledge them in bulk, and a live region confirms the change.
0 selected
| Elevated 5xx on checkout | billing | sev1 | open | |
| Slow queries on search | search | sev2 | open | |
| Cache hit rate dropped | api-gateway | sev2 | open | |
| Webhook retries spiking | integrations | sev3 | acknowledged |
Show sourceexamples/data-table/recipe-ops-dashboard.tsx
"use client";
import { useState } from "react";
import {
Button,
DataTable,
type DataTableColumnDef,
type DataTableRowSelectionState,
} from "@dethink/components";
type Incident = {
id: string;
title: string;
service: string;
severity: "sev1" | "sev2" | "sev3";
status: "open" | "acknowledged" | "resolved";
};
const initialIncidents: Incident[] = [
{
id: "i1",
title: "Elevated 5xx on checkout",
service: "billing",
severity: "sev1",
status: "open",
},
{
id: "i2",
title: "Slow queries on search",
service: "search",
severity: "sev2",
status: "open",
},
{
id: "i3",
title: "Webhook retries spiking",
service: "integrations",
severity: "sev3",
status: "acknowledged",
},
{
id: "i4",
title: "Cache hit rate dropped",
service: "api-gateway",
severity: "sev2",
status: "open",
},
{
id: "i5",
title: "Cert expiring in 7 days",
service: "edge",
severity: "sev3",
status: "open",
},
];
const severityTone: Record<Incident["severity"], string> = {
sev1: "bg-destructive/10 text-destructive",
sev2: "bg-warning/15 text-warning",
sev3: "bg-muted text-muted-foreground",
};
const columns: DataTableColumnDef<Incident>[] = [
{ accessorKey: "title", header: "Incident" },
{ accessorKey: "service", header: "Service" },
{
accessorKey: "severity",
header: "Severity",
cell: ({ row }) => (
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium uppercase ${severityTone[row.original.severity]}`}
>
{row.original.severity}
</span>
),
},
{ accessorKey: "status", header: "Status" },
];
/**
* The full feature set in one seat: sorting, filtering, selection, and
* pagination together, with a bulk action operating on the selected rows
* and a live region confirming what changed.
*/
export function DataTableRecipeOpsDashboard() {
const [incidents, setIncidents] = useState(initialIncidents);
const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>(
{},
);
const [statusMessage, setStatusMessage] = useState("");
const selectedIds = Object.keys(rowSelection).filter(
(id) => rowSelection[id],
);
return (
<div className="space-y-3">
<div className="flex items-center justify-between gap-3">
<p aria-live="polite" className="text-muted-foreground text-sm">
{statusMessage || `${selectedIds.length} selected`}
</p>
<Button
size="sm"
disabled={selectedIds.length === 0}
onClick={() => {
setIncidents((current) =>
current.map((incident) =>
selectedIds.includes(incident.id)
? { ...incident, status: "acknowledged" }
: incident,
),
);
setStatusMessage(`Acknowledged ${selectedIds.length} incident(s).`);
setRowSelection({});
}}
>
Acknowledge selected
</Button>
</div>
<DataTable
columns={columns}
data={incidents}
getRowId={(row) => row.id}
selectionMode="multiple"
rowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
enableGlobalFilter
globalFilterPlaceholder="Filter incidents…"
enablePagination
defaultPagination={{ pageIndex: 0, pageSize: 4 }}
pageSizeOptions={[4, 8]}
defaultSorting={[{ id: "severity", desc: false }]}
/>
</div>
);
}Every stateful feature is controllable (value + onChange) or uncontrolled (default value), and manual modes hand the work to your server.
| Prop | What it does | Default |
|---|---|---|
columnsDataTableColumnDef<TData>[] | TanStack-style column definitions: accessorKey, header, and an optional cell renderer. | Not set |
dataTData[] | Row data; pair with getRowId for stable selection keys. | Not set |
enableSortingboolean | Sortable headers with accessible sort buttons; control with sorting/onSortingChange or seed defaultSorting. | true |
enableGlobalFilterboolean | Toolbar text filter across all columns; globalFilterPlaceholder labels it. | false |
enableColumnFilters / renderColumnFilterboolean / (column) => ReactNode | Per-column filtering with a custom filter UI per column. | false / — |
selectionMode"none" | "single" | "multiple" | Row selection with header select-all in multiple mode; control with rowSelection/onRowSelectionChange. | "none" |
enablePaginationboolean | Pagination footer with pageSizeOptions; control with pagination/onPaginationChange. | false |
loading / loadingContentboolean / ReactNode | Loading state announced to assistive tech. | false / built-in |
error / emptyContentReactNode | Error and empty states rendered inside the table region. | Not set |
manualSorting / manualFiltering / manualPaginationboolean | Server-driven mode: the table emits state changes and renders what you pass, with pageCount/rowCount. | false |
rowFilter(row) => boolean | Keeps only matching rows before sorting and pagination, with row ids tied to the source index so selection survives. Pair it with createFilterPredicate from FilterBar. | Not set |
toolbarReactNode | Content at the start of the toolbar, such as a FilterBar. | Not set |
globalFilterFnFilterFnOption | Custom TanStack global filter function for the search box. | "includesString" |
renderRowActions(row) => ReactNode | Trailing actions cell per row — menus, buttons, links. | Not set |
density / caption / labelsdensity scale / ReactNode / DataTableLabels | Visual density, an accessible caption, and overridable UI strings for localization. | Not set |