ShadTable
DiscordGitHub
ShadTable

A collection of composable table components built on shadcn/ui and TanStack Table.

Data Table

A sortable, filterable, paginated table with a composable toolbar for column-specific filters. Sorting, filtering, and pagination all happen entirely in the browser — the whole dataset is sent to the client once, and TanStack Table slices it locally.

npx shadcn add https://shad-table.dev/r/data-table.json
Name
Email
Role
Status
Ava Thompsonava.t@example.comAdminActive
Liam Chenliam.chen@example.comEditorActive
Sofia Patelsofia.p@example.comViewerInactive
Noah Garcianoah.g@example.comEditorActive
Mia Johnsonmia.j@example.comAdminPending
Ethan Kimethan.kim@example.comViewerActive
Isabella Rossiisabella.r@example.comEditorActive
Lucas Martinlucas.m@example.comAdminInactive
Amelia Novakamelia.n@example.comViewerActive
Mason Leemason.lee@example.comEditorPending

How it works

1.The whole dataset lives on the client

data is passed straight in as a prop — there's no server call. getFilteredRowModel, getSortedRowModel, and getPaginationRowModel all run in the browser against that full in-memory array.

src/components/basic/data-table.tsx
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
// ...
})

2.Global search filters every column at once

The search Input calls table.setGlobalFilter on every keystroke. TanStack Table matches that value against all columns' rendered text and getFilteredRowModel re-filters the in-memory rows — no debounce, no server round-trip.

src/components/basic/data-table.tsx
<Input
type="text"
placeholder="Search..."
onChange={(e) => table.setGlobalFilter(e.target.value)}
className="filter-input w-64"
/>

3.Per-column filters are just column.setFilterValue

DataTableFilter is a thin Select wrapper around the column API — it reads column.getFilterValue() and writes back with column.setFilterValue(). BasicTableUsage composes one per filterable column through the table's filters render-prop.

src/components/DataTableFilter.tsx
const filterValue = column?.getFilterValue() as string | undefined
<Select
onValueChange={(val) => column?.setFilterValue(val)}
value={filterValue}
>
{/* ...options */}
</Select>

4.Clicking a header sorts in place

Each sortable TableHead wires its onClick to column.getToggleSortingHandler(). TanStack re-sorts the already-filtered rows via getSortedRowModel — sorting, filtering, and pagination all compose on the same in-memory row list.

src/components/basic/data-table.tsx
<TableHead
onClick={sortHandler}
role={canSort ? 'button' : undefined}
className={canSort ? 'cursor-pointer select-none' : undefined}
>
{/* header content + sort icon */}
</TableHead>

5.Page size is just table.setPageSize

The page-size Select calls table.setPageSize directly, which recalculates pageIndex for you so the current scroll position stays sane. getPaginationRowModel then slices the filtered/sorted rows into pages, all without leaving the browser.

src/components/basic/data-table.tsx
<Select
value={pagination.pageSize.toString()}
onValueChange={(val) => table.setPageSize(Number(val))}
>
{/* ...page size options */}
</Select>