A sortable, filterable, paginated table with column visibility, row selection, sticky and reorderable columns, and loading states, built on TanStack Table v9.
| Status | ||||
|---|---|---|---|---|
| / | indexed | 48,210 | 31,400 | 42% |
| /pricing | indexed | 21,480 | 16,200 | 38% |
| /docs | indexed | 18,930 | 9,800 | 21% |
| /docs/installation | indexed | 12,310 | 7,900 | 19% |
| /blog/launch-week | indexed | 9,120 | 8,400 | 61% |
| /changelog | indexed | 4,305 | 2,900 | 33% |
| /careers | indexed | 1,870 | 1,600 | 55% |
| /legacy/signup | redirect | 940 | 910 | 90% |
1–8 of 12
"use client"
import { Badge } from "@/components/ui/badge"Installation#
pnpm dlx shadcn@latest add https://dashboardcn.com/r/data-table.jsonAlso installs table, button, input, dropdown-menu, checkbox, skeleton from shadcn/ui if missing.
Usage#
import {
DataTable,
DataTableColumnHeader,
createDataTableColumnHelper,
} from "@/components/ui/data-table"
const helper = createDataTableColumnHelper<Row>()
const columns = helper.columns([
helper.accessor("path", {
header: ({ column }) => <DataTableColumnHeader column={column} title="Page" />,
}),
helper.accessor("views", {
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Views" align="right" />
),
cell: ({ row }) => <div className="text-right">{row.original.views}</div>,
}),
])
<DataTable columns={columns} data={rows} searchKey="path" />
// Or own the instance and lay the parts out yourself.
const table = useDataTable({ columns, data: rows, pageSize: 5 })
<DataTableToolbar>
<DataTableSearch table={table} column="path" />
<DataTableViewOptions table={table} className="ml-auto" />
</DataTableToolbar>
<DataTableContent table={table} pending={isFetching} stickyHeader maxHeight={340} />
<DataTablePagination table={table} pageSizeOptions={[5, 10, 25]} />Examples#
Loading and refreshing#
loading swaps the rows for a skeleton. pending keeps the rows and dims them, which is what a filter change wants — the table reports it is busy without collapsing and jumping.
| Organic search | 74,910 | 1,684 | $98.4K |
| Paid search | 38,260 | 1,192 | $81.1K |
| Direct | 31,540 | 623 | $45.9K |
| Referral | 13,080 | 388 | $30.1K |
8,940 | 574 | $38.8K |
"use client"
import * as React from "react"Row selection#
Put createSelectionColumn() first in the column list and set enableRowSelection. A bar with your actions appears while rows are selected. Pass getRowId so a selection survives sorting and paging, and shift-click a checkbox to take a range.
| Status | |||||
|---|---|---|---|---|---|
| INV-2841 | Northwind Traders | paid | 2026-08-02 | $12,400.00 | |
| INV-2842 | Contoso | open | 2026-08-04 | $3,180.00 | |
| INV-2843 | Fabrikam | overdue | 2026-07-11 | $8,960.00 | |
| INV-2844 | Tailspin Toys | paid | 2026-08-09 | $1,240.00 | |
| INV-2845 | Adventure Works | open | 2026-08-12 | $22,500.00 | |
| INV-2846 | Proseware | paid | 2026-08-14 | $640.00 | |
| INV-2847 | Wide World Importers | overdue | 2026-06-28 | $15,820.00 | |
| INV-2848 | Lucerne Publishing | open | 2026-08-19 | $4,075.00 |
Shift-click a checkbox to select a range. Selected: none
"use client"
import * as React from "react"Row actions#
A trailing column with a ⋯ menu. Right-clicking anywhere in a row offers the same actions through shadcn's ContextMenu — not a table feature: renderRow wraps the row in a trigger, and one array of actions feeds both menus. Drop it if you do not want it.
| Scope | Actions | ||
|---|---|---|---|
| Production server | admin | 2 minutes ago | |
| Staging server | write | 1 hour ago | |
| Analytics export | read | Yesterday | |
| CI pipeline | write | 3 days ago | |
| Partner sandbox | read | 2 weeks ago | |
| Legacy webhook | read | Never |
Right-click a row for the same actions as its ⋯ button.
"use client"
import * as React from "react"Sticky columns#
pinnedColumns holds columns against either edge while the rest scroll sideways, so the actions menu stays reachable at any scroll position. stickyHeader and maxHeight do the same vertically. Pinned columns need a size on the column def, because the sticky offsets are measured from it. The right-click menu from the previous example is in here too — it is written in the example, not a table feature, so take it or leave it.
| Status | Actions | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Spring launch — brand | Search | live | 1.28M | 38,400 | 2.99% | $1.42 | $54.5K | 1,842 | $29.60 | 4.1× | |
| Spring launch — retarget | Display | live | 2.94M | 21,600 | 0.73% | $0.68 | $14.7K | 612 | $24.00 | 3.4× | |
| Always-on — competitor | Search | live | 486K | 19,200 | 3.95% | $2.86 | $54.9K | 908 | $60.50 | 2.2× | |
| Lifecycle — winback | live | 312K | 41,800 | 13.4% | $0.04 | $1.67K | 1,204 | $1.40 | 18.9× | ||
| Creator program | Social | paused | 1.82M | 47,300 | 2.6% | $0.91 | $43K | 1,390 | $31.00 | 3.8× | |
| Podcast — Q1 flight | Audio | paused | 940K | 8,200 | 0.87% | $3.40 | $27.9K | 318 | $87.70 | 1.6× | |
| Marketplace listings | Partner | live | 128K | 6,400 | 5% | $1.15 | $7.36K | 486 | $15.10 | 6.7× | |
| Docs — long tail | Search | live | 74K | 9,100 | 12.3% | $0.52 | $4.73K | 402 | $11.80 | 8.4× | |
| Enterprise ABM | Display | draft | 0 | 0 | 0% | $0.00 | $0 | 0 | $0.00 | 0.0× | |
| Conference — booth QR | Offline | paused | 12K | 1,900 | 15.8% | $0.00 | $18K | 96 | $187.50 | 0.9× | |
| Newsletter sponsorships | live | 620K | 14,400 | 2.32% | $1.04 | $15K | 508 | $29.50 | 4.6× | ||
| App install — iOS | Social | live | 3.41M | 88,200 | 2.59% | $0.47 | $41.5K | 3,120 | $13.30 | 5.2× |
"use client"
import { MoreHorizontal } from "lucide-react"Reorderable columns#
reorderable puts a handle on each header. Drag one header onto another to move it, or focus a handle and use the arrow keys. Pass an array of column ids to limit which columns move.
Tier | |||||
|---|---|---|---|---|---|
| North America | enterprise | 184 | 24,310 | $8.42M | 4.1% |
| EMEA | enterprise | 141 | 17,820 | $6.18M | 5.2% |
| APAC | growth | 96 | 8,940 | $2.74M | 6.8% |
| LATAM | growth | 62 | 4,120 | $1.18M | 8.1% |
| Nordics | starter | 48 | 2,060 | $486K | 9.4% |
| Benelux | starter | 39 | 1,540 | $361K | 7.7% |
| ANZ | growth | 34 | 2,880 | $812K | 5.8% |
"use client"
import { Badge } from "@/components/ui/badge"Compact#
density="compact" tightens the rows. With the toolbar and pagination off the table is only rows, and onRowClick with rowClassName makes them behave like a list.
| Query | Clicks | Position | Change |
|---|---|---|---|
| dashboard components | 4,120 | 1.4 | +18.0% |
| shadcn kpi card | 3,480 | 2.1 | +32.0% |
| react data table | 2,910 | 4.8 | -6.0% |
| tanstack table example | 2,140 | 3.2 | +9.0% |
| funnel chart react | 1,880 | 5.6 | +21.0% |
| analytics ui kit | 1,240 | 8.1 | -14.0% |
| sparkline component | 960 | 6.4 | +4.0% |
| activity heatmap react | 720 | 9.2 | +41.0% |
"use client"
import * as React from "react"Composed#
DataTable is a preset over parts that are all exported. Call useDataTable yourself and place the toolbar, search, view options, content, and pagination wherever the design puts them.
| Requester | Status | Priority | ||
|---|---|---|---|---|
| Billing address will not save | R. Okafor | open | high | 2h |
| SAML login loop after rotation | M. Lindqvist | open | urgent | 4h |
| Export misses the last row | J. Alvarez | waiting | normal | 1d |
| Webhook retries fire twice | P. Nakamura | open | high | 1d |
| Invite email lands in spam | S. Dube | closed | normal | 3d |
1–5 of 9
"use client"
import { Badge } from "@/components/ui/badge"