dashboardcn
1

Data Table

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%
/pricingindexed
21,480
16,200
38%
/docsindexed
18,930
9,800
21%
/docs/installationindexed
12,310
7,900
19%
/blog/launch-weekindexed
9,120
8,400
61%
/changelogindexed
4,305
2,900
33%
/careersindexed
1,870
1,600
55%
/legacy/signupredirect
940
910
90%

18 of 12

Page 1 of 2
"use client"

import { Badge } from "@/components/ui/badge"

Installation

pnpm dlx shadcn@latest add https://dashboardcn.com/r/data-table.json

Also 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
Email
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-2841Northwind Traderspaid2026-08-02
$12,400.00
INV-2842Contosoopen2026-08-04
$3,180.00
INV-2843Fabrikamoverdue2026-07-11
$8,960.00
INV-2844Tailspin Toyspaid2026-08-09
$1,240.00
INV-2845Adventure Worksopen2026-08-12
$22,500.00
INV-2846Prosewarepaid2026-08-14
$640.00
INV-2847Wide World Importersoverdue2026-06-28
$15,820.00
INV-2848Lucerne Publishingopen2026-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 serveradmin2 minutes ago
Staging serverwrite1 hour ago
Analytics exportreadYesterday
CI pipelinewrite3 days ago
Partner sandboxread2 weeks ago
Legacy webhookreadNever

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 — brandSearchlive
1.28M
38,400
2.99%
$1.42
$54.5K
1,842
$29.60
4.1×
Spring launch — retargetDisplaylive
2.94M
21,600
0.73%
$0.68
$14.7K
612
$24.00
3.4×
Always-on — competitorSearchlive
486K
19,200
3.95%
$2.86
$54.9K
908
$60.50
2.2×
Lifecycle — winbackEmaillive
312K
41,800
13.4%
$0.04
$1.67K
1,204
$1.40
18.9×
Creator programSocialpaused
1.82M
47,300
2.6%
$0.91
$43K
1,390
$31.00
3.8×
Podcast — Q1 flightAudiopaused
940K
8,200
0.87%
$3.40
$27.9K
318
$87.70
1.6×
Marketplace listingsPartnerlive
128K
6,400
5%
$1.15
$7.36K
486
$15.10
6.7×
Docs — long tailSearchlive
74K
9,100
12.3%
$0.52
$4.73K
402
$11.80
8.4×
Enterprise ABMDisplaydraft
0
0
0%
$0.00
$0
0
$0.00
0.0×
Conference — booth QROfflinepaused
12K
1,900
15.8%
$0.00
$18K
96
$187.50
0.9×
Newsletter sponsorshipsEmaillive
620K
14,400
2.32%
$1.04
$15K
508
$29.50
4.6×
App install — iOSSociallive
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 Americaenterprise
184
24,310
$8.42M
4.1%
EMEAenterprise
141
17,820
$6.18M
5.2%
APACgrowth
96
8,940
$2.74M
6.8%
LATAMgrowth
62
4,120
$1.18M
8.1%
Nordicsstarter
48
2,060
$486K
9.4%
Beneluxstarter
39
1,540
$361K
7.7%
ANZgrowth
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.

Support queue
Unresolved tickets across all plans.
RequesterStatusPriority
Billing address will not saveR. Okaforopenhigh
2h
SAML login loop after rotationM. Lindqvistopenurgent
4h
Export misses the last rowJ. Alvarezwaitingnormal
1d
Webhook retries fire twiceP. Nakamuraopenhigh
1d
Invite email lands in spamS. Dubeclosednormal
3d

15 of 9

Page 1 of 2
"use client"

import { Badge } from "@/components/ui/badge"