Data Table
A generic Tanstack-powered data table compound that composes Visor's table primitive. Handles sorting, pagination, row selection, global filter, sticky header, loading, and empty states. Built with CSS Modules — copy it into your project and own it completely.
Basic
| Ada Lovelace | ada@example.com | Admin |
| Bjarne Stroustrup | bjarne@example.com | Editor |
| Carmack | carmack@example.com | Viewer |
| Dennis Ritchie | dennis@example.com | Admin |
| Edsger Dijkstra | edsger@example.com | Editor |
Density
Use density to step row vertical padding. The default matches existing behaviour (12px); compact (8px) packs more rows into a narrow viewport; editorial (20px) makes each row read as a card for high-design admin patterns. Horizontal cell padding is unchanged across densities. Themes can override per-density values by targeting [data-density="…"] from their own selectors.
| Ada Lovelace | ada@example.com | Admin |
| Bjarne Stroustrup | bjarne@example.com | Editor |
| Carmack | carmack@example.com | Viewer |
| Dennis Ritchie | dennis@example.com | Admin |
| Edsger Dijkstra | edsger@example.com | Editor |
| Ada Lovelace | ada@example.com | Admin |
| Bjarne Stroustrup | bjarne@example.com | Editor |
| Carmack | carmack@example.com | Viewer |
| Dennis Ritchie | dennis@example.com | Admin |
| Edsger Dijkstra | edsger@example.com | Editor |
| Ada Lovelace | ada@example.com | Admin |
| Bjarne Stroustrup | bjarne@example.com | Editor |
| Carmack | carmack@example.com | Viewer |
| Dennis Ritchie | dennis@example.com | Admin |
| Edsger Dijkstra | edsger@example.com | Editor |
Row Selection
| Ada Lovelace | ada@example.com | Admin | |
| Bjarne Stroustrup | bjarne@example.com | Editor | |
| Carmack | carmack@example.com | Viewer | |
| Dennis Ritchie | dennis@example.com | Admin | |
| Edsger Dijkstra | edsger@example.com | Editor |
Group Rows
Use rows instead of data when the server pre-groups results — calendar-grouped events, status-grouped tasks, date-ranged admin logs. Each entry is either a data row ({ kind: "data", id, row }) or a group-head separator ({ kind: "group", id, label, count? }). When rows is provided, column sort controls and the pagination footer are suppressed — the caller owns grouping and windowing.
| Name | Role | |
|---|---|---|
| Tonight · Sat Apr 272 | ||
| Ada Lovelace | ada@example.com | Admin |
| Bjarne Stroustrup | bjarne@example.com | Editor |
| This week · Apr 28 — May 43 | ||
| Carmack | carmack@example.com | Viewer |
| Dennis Ritchie | dennis@example.com | Admin |
| Edsger Dijkstra | edsger@example.com | Editor |
Pass groupRowRenderer to replace the default eyebrow-style header with custom content — a badge, an icon, or any React node.
| Name | Role | |
|---|---|---|
| Tonight · Sat Apr 27(2) | ||
| Ada Lovelace | ada@example.com | Admin |
| Bjarne Stroustrup | bjarne@example.com | Editor |
| This week · Apr 28 — May 4(3) | ||
| Carmack | carmack@example.com | Viewer |
| Dennis Ritchie | dennis@example.com | Admin |
| Edsger Dijkstra | edsger@example.com | Editor |
Loading State
Empty State
No results | ||
Installation
npx visor add data-tableThis copies two files into your project and installs @tanstack/react-table:
components/ui/data-table/data-table.tsx— the componentcomponents/ui/data-table/data-table.module.css— the styles
Usage
import { DataTable, type ColumnDef } from '@/components/ui/data-table/data-table';
interface User {
id: string;
name: string;
email: string;
role: string;
}
const columns: ColumnDef<User>[] = [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
{ accessorKey: 'role', header: 'Role' },
];
export default function UsersPage({ users }: { users: User[] }) {
return <DataTable columns={columns} data={users} enableRowSelection />;
}API Reference
DataTableProps
| Prop | Type | Default | Description |
|---|---|---|---|
columns* | ColumnDef<TData, TValue>[] | — | Tanstack column definitions. Each entry drives the header, cell renderer, sort behavior, and column id. |
data* | TData[] | — | Array of row records to render. |
sorting | SortingState | — | Controlled sorting state. Omit for uncontrolled usage. |
onSortingChange | OnChangeFn<SortingState> | — | Called when sorting state changes. |
defaultSorting | SortingState | — | Uncontrolled initial sorting state. |
pagination | PaginationState | — | Controlled pagination state. |
onPaginationChange | OnChangeFn<PaginationState> | — | Called when pagination state changes. |
pageSize | number | 10 | Initial page size when pagination is uncontrolled. |
pageSizeOptions | number[] | [10, 25, 50, 100] | Options for the rows-per-page select. |
enableRowSelection | boolean | false | When true, injects a checkbox column at position 0 with select-all and per-row checkboxes. |
rowSelection | RowSelectionState | — | Controlled row selection state. |
onRowSelectionChange | OnChangeFn<RowSelectionState> | — | Called when row selection state changes. |
getRowId | (row: TData, index: number) => string | — | Custom stable row id getter. Defaults to index-based ids when omitted. |
globalFilter | string | — | Controlled global filter value. |
onGlobalFilterChange | (value: string) => void | — | Called when the global filter value changes. |
loading | boolean | false | When true, renders skeleton rows in place of data rows. |
emptyState | React.ReactNode | — | Override for the default EmptyState slot rendered when data is empty. |
stickyHeader | boolean | false | When true, the thead is positioned sticky at the top of the scroll container. |
className | string | — | Additional CSS class names merged onto the root div. |
The component also accepts all standard HTML attributes for the root <div>.
Source Files
After running npx visor add data-table, you'll have:
data-table.tsx
A "use client" component that wraps useReactTable from @tanstack/react-table and composes the existing Visor Table, TableHeader, TableBody, TableRow, TableHead, and TableCell primitives. Sorting, pagination, row selection, and global filter are all supported in either controlled or uncontrolled modes. The component re-exports the useful Tanstack types (ColumnDef, SortingState, RowSelectionState, PaginationState) so consumers don't need a second import.
data-table.module.css
All values use CSS custom properties from @loworbitstudio/visor-core, so the table automatically adapts to your active theme. The root uses container-type: inline-size so the pagination footer stacks vertically inside narrow containers without relying on viewport media queries. Sort headers use native <button> elements inside the <th> so screen readers announce them correctly; the parent <th> carries aria-sort.
Customization
After copying the component, you own it completely. Common customizations:
- Swap the native
<select>page-size picker for Visor'sSelectcompound if you want Radix-consistent styling across the app. - Add server-driven pagination by passing
paginationandonPaginationChangeand wiring them to your data fetch. - Extend the selection column with a bulk-action bar by reading
rowSelectionfrom the controlled state. - Add column-visibility toggles by layering Tanstack's
getVisibilityState-style APIs.
Confirm Dialog
Admin confirmation dialog compound wrapping Dialog with severity-driven icon and color, async-aware confirm handler, and an optional confirm-text gate for high-stakes destructive actions. Built with CSS Modules and container queries — copy it into your project and own it completely.
Empty State
Admin placeholder compound for lists, tables, search results, and dashboard regions with no data. Icon, heading, description, primary and secondary action slots. Built with CSS Modules and container queries — copy it into your project and own it completely.