Data TableNew
A clean table with tabs, search, date range and multi-select filters, row selection, column toggles, sorting, pagination, pinned columns, and loading, empty and error states.
Preview
Services
Deploys and uptime across every region.
| Actions | |||||||
|---|---|---|---|---|---|---|---|
auth-apiPlatform | Healthy | us-east | 12 | 99.20% | |||
billing-apiCore | Down | eu-west | 49 | 99.33% | |||
search-apiGrowth | Healthy | ap-south | 86 | 99.46% | |||
gateway-apiPayments | Degraded | us-east | 123 | 99.59% | |||
notifications-apiPlatform | Healthy | eu-west | 160 | 99.72% | |||
media-apiCore | Healthy | ap-south | 17 | 99.85% | |||
analytics-apiGrowth | Healthy | us-east | 54 | 99.98% | |||
checkout-apiPayments | Down | eu-west | 91 | 99.31% | |||
profiles-apiPlatform | Healthy | ap-south | 128 | 99.44% | |||
inventory-apiCore | Degraded | us-east | 165 | 99.57% |
Setup
Install with the shadcn CLI
One command adds every file, the npm packages and any Kinetik components it builds on. Needs a components.json; the CLI offers to create one if your project has none.
Or install manually
- 1
Run in your terminal:
$npm install clsx tailwind-merge @radix-ui/react-slot class-variance-authority framer-motion lucide-react - 2
Copy
lib/utils.tsfrom the Code tab. Skip if you already havecn(). - 3
Copy
styles/tokens.cssfrom the Code tab and import it in your global CSS after Tailwind:@import "./styles/tokens.css"; - 4
This component uses Button, Menubar and Sliding Tabs. Copy
components/ui/button/,components/ui/menubar/andcomponents/ui/sliding-tabs/from the Code tab too, at the same path so the import resolves. Skip any you already have. - 5
Create a
components/ui/data-table/folder and copy every file from the Code tab into it. Each file does one job, so you can read or change a piece without the rest. - 6
Import and render:
Exampleimport { DataTable } from "@/components/ui/data-table" <DataTable data={rows} columns={[{ key: "name", header: "Name", sortable: true }]} />
Usage
Describe the columns and pass your rows. Out of the box the table searches, filters, sorts and pages for you.
import { DataTable, type DataTableColumn } from "@/components/ui/data-table"
type User = { id: number; name: string; role: string; joined: string }
const columns: DataTableColumn<User>[] = [
{ key: "name", header: "Name", sortable: true, sticky: "left" },
{ key: "role", header: "Role", sortable: true },
{ key: "joined", header: "Joined", align: "right" },
]
export function Users({ users }: { users: User[] }) {
return (
<DataTable
data={users}
columns={columns}
rowKey={(user) => user.id}
title="Team"
searchable
filters={[{ key: "role", label: "Role", options: ["Admin", "Editor", "Viewer"] }]}
/>
)
}
Custom cells
cell renders anything. Sorting and search still use the raw value, or value if you need
something else, such as a date as a number.
{
key: "joined",
header: "Joined",
sortable: true,
value: (user) => new Date(user.joined).getTime(),
cell: (user) => <time dateTime={user.joined}>{formatDate(user.joined)}</time>,
}
Tabs, dates, selection and columns
Every part of the toolbar is opt-in, so turn on only what your page needs.
<DataTable
data={orders}
columns={columns}
rowKey={(order) => order.id}
tabs={{ key: "status", options: ["Pending", "In transit", "Delivered"] }}
dateRange={{ key: "shippedAt", label: "Any ship date" }}
selectable
onSelectionChange={(ids) => setSelectedIds(ids)}
columnToggle
pageSizeOptions={[10, 25, 50]}
/>
tabsshows one value of a field at a time, with an "All" tab first.dateRangekeeps rows whose field falls in the range. Values can beDates, timestamps or ISO strings.selectableadds a checkbox to each row and one in the header for the page. Picks survive paging, searching and filtering, so passrowKey.columnToggleadds a menu to show and hide columns. Pinned columns always stay.
Bulk delete and row actions
While rows are picked, a bar slides in over the header with the count, Delete and Clear. Delete
asks first; deleting every row also asks you to type delete. Remove the rows from your data in
onDeleteRows.
Give each row a ⋯ menu with RowActions. It takes the same entries as the
Menubar, so labels, separators, icons and red destructive items all
work, and it opens above the row when there's no room below.
import { ConfirmDialog, DataTable, RowActions } from "@/components/ui/data-table"
<DataTable
data={services}
rowKey={(s) => s.id}
selectable
rowNoun={["service", "services"]}
onDeleteRows={(rows) => removeServices(rows.map((s) => s.id))}
columns={[
...columns,
{
key: "actions",
header: <span className="sr-only">Actions</span>,
sticky: "right",
cell: (s) => (
<RowActions
label={`Actions for ${s.name}`}
items={[
{ label: "Redeploy", onSelect: () => redeploy(s.id) },
{ type: "separator" },
{ label: "Delete", destructive: true, onSelect: () => setDeleting(s) },
]}
/>
),
},
]}
/>
ConfirmDialog is exported too, so a single-row Delete can ask the same way.
Server-side data
Set manual when your API does the work. The table shows data as it is and tells you what changed:
search waits 300ms after typing stops, and every change returns to page 1.
<DataTable
manual
data={page.rows}
pageCount={page.pageCount}
page={pageNumber}
onPageChange={setPageNumber}
onSearchChange={setQuery}
onFiltersChange={setFilters}
onSortChange={setSort}
onTabChange={setStatus}
onDateRangeChange={setDates}
onPageSizeChange={setPageSize}
loading={isFetching}
columns={columns}
searchable
/>
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
data | T[] | — | The rows. |
columns | DataTableColumn<T>[] | — | See below. |
rowKey | (row, index) => key | Index | A stable key per row. |
title | ReactNode | — | Shown above the table, with an optional description. |
searchable | boolean | false | Shows a search box. Narrow it with searchKeys. |
filters | { key; label; options }[] | — | Multi-select filters. Choosing none shows everything. |
tabs | { key; options; allLabel? } | — | Tabs that show one value of a field, plus All. With defaultTab and onTabChange. |
dateRange | { key; label? } | — | A date range picker with presets. With defaultDateRange and onDateRangeChange. |
selectable | boolean | false | Row checkboxes, and select-all for the page. Read picks with onSelectionChange. |
onDeleteRows | (rows) => void | — | Adds Delete to the selection bar, behind a confirmation. |
bulkActions | (rows, clear) => ReactNode | — | More buttons for the selection bar. |
rowNoun | [string, string] | ["row", "rows"] | What a row is called in the confirmation. |
columnToggle | boolean | false | A menu to show and hide columns. Start some hidden with defaultHiddenColumns. |
sort | { key; direction } | null | — | Controlled sort. Or defaultSort to start sorted. |
scrollable | boolean | true | Keep columns full width and scroll sideways. Off, the table fits its box and text wraps. |
pageSize | number | 10 | Rows per page. |
pageSizeOptions | number[] | — | Shows a rows-per-page picker, e.g. [10, 25, 50]. With onPageSizeChange. |
page | number | — | Controlled page, with onPageChange. |
manual | boolean | false | Data is already searched, filtered, sorted and paged. Pass pageCount. |
loading | boolean | false | Shows skeleton rows. |
error | ReactNode | — | Shown in place of the rows. |
emptyMessage | ReactNode | "No results" | Shown when there are no rows. |
Columns
| Field | Type | Notes |
|---|---|---|
key | string | Field on the row, and the column's id. |
header | ReactNode | Column title. Add muted text beside it with subtitle, e.g. a unit. |
align | "left" | "center" | "right" | Right-align numbers. |
sortable | boolean | Click cycles ascending, descending, unsorted. |
sticky | "left" | "right" | Keeps the column in view while the table scrolls sideways. |
cell | (row) => ReactNode | Custom cell. |
value | (row) => unknown | What sorting and search use, when it differs from the field. |
minWidth | number | string | Minimum column width. |
Accessibility
- A real
tablewithth scope="col"; sorted columns announcearia-sort - Sort controls are buttons, so headers sort from the keyboard
- Filters, the columns menu and row selection use real checkboxes; Escape or a click outside closes menus
- Tabs come from Sliding Tabs, so arrow keys move between them
- Selected rows set
aria-selected, and the page checkbox reports a mixed state when some rows are picked - The selection bar is a labelled toolbar and announces the count as it changes
- The confirmation is a modal
alertdialog: focus stays inside, starts on Cancel, and Escape cancels - Row menus are real
menus: arrow keys, Home, End and Escape work, and focus returns to the ⋯ button - Pagination is a labelled
nav, and the current page hasaria-current - Loading rows set
aria-busyon the table body