VisorVisor
ComponentsAdmin

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 Lovelaceada@example.comAdmin
Bjarne Stroustrupbjarne@example.comEditor
Carmackcarmack@example.comViewer
Dennis Ritchiedennis@example.comAdmin
Edsger Dijkstraedsger@example.comEditor

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 Lovelaceada@example.comAdmin
Bjarne Stroustrupbjarne@example.comEditor
Carmackcarmack@example.comViewer
Dennis Ritchiedennis@example.comAdmin
Edsger Dijkstraedsger@example.comEditor
Ada Lovelaceada@example.comAdmin
Bjarne Stroustrupbjarne@example.comEditor
Carmackcarmack@example.comViewer
Dennis Ritchiedennis@example.comAdmin
Edsger Dijkstraedsger@example.comEditor
Ada Lovelaceada@example.comAdmin
Bjarne Stroustrupbjarne@example.comEditor
Carmackcarmack@example.comViewer
Dennis Ritchiedennis@example.comAdmin
Edsger Dijkstraedsger@example.comEditor

Row Selection

Ada Lovelaceada@example.comAdmin
Bjarne Stroustrupbjarne@example.comEditor
Carmackcarmack@example.comViewer
Dennis Ritchiedennis@example.comAdmin
Edsger Dijkstraedsger@example.comEditor

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.

NameEmailRole
Tonight · Sat Apr 272
Ada Lovelaceada@example.comAdmin
Bjarne Stroustrupbjarne@example.comEditor
This week · Apr 28 — May 43
Carmackcarmack@example.comViewer
Dennis Ritchiedennis@example.comAdmin
Edsger Dijkstraedsger@example.comEditor

Pass groupRowRenderer to replace the default eyebrow-style header with custom content — a badge, an icon, or any React node.

NameEmailRole
Tonight · Sat Apr 27(2)
Ada Lovelaceada@example.comAdmin
Bjarne Stroustrupbjarne@example.comEditor
This week · Apr 28 — May 4(3)
Carmackcarmack@example.comViewer
Dennis Ritchiedennis@example.comAdmin
Edsger Dijkstraedsger@example.comEditor

Loading State

Empty State

No results

Installation

npx visor add data-table

This copies two files into your project and installs @tanstack/react-table:

  • components/ui/data-table/data-table.tsx — the component
  • components/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

PropTypeDefaultDescription
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.
sortingSortingStateControlled sorting state. Omit for uncontrolled usage.
onSortingChangeOnChangeFn<SortingState>Called when sorting state changes.
defaultSortingSortingStateUncontrolled initial sorting state.
paginationPaginationStateControlled pagination state.
onPaginationChangeOnChangeFn<PaginationState>Called when pagination state changes.
pageSizenumber10Initial page size when pagination is uncontrolled.
pageSizeOptionsnumber[][10, 25, 50, 100]Options for the rows-per-page select.
enableRowSelectionbooleanfalseWhen true, injects a checkbox column at position 0 with select-all and per-row checkboxes.
rowSelectionRowSelectionStateControlled row selection state.
onRowSelectionChangeOnChangeFn<RowSelectionState>Called when row selection state changes.
getRowId(row: TData, index: number) => stringCustom stable row id getter. Defaults to index-based ids when omitted.
globalFilterstringControlled global filter value.
onGlobalFilterChange(value: string) => voidCalled when the global filter value changes.
loadingbooleanfalseWhen true, renders skeleton rows in place of data rows.
emptyStateReact.ReactNodeOverride for the default EmptyState slot rendered when data is empty.
stickyHeaderbooleanfalseWhen true, the thead is positioned sticky at the top of the scroll container.
classNamestringAdditional 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's Select compound if you want Radix-consistent styling across the app.
  • Add server-driven pagination by passing pagination and onPaginationChange and wiring them to your data fetch.
  • Extend the selection column with a bulk-action bar by reading rowSelection from the controlled state.
  • Add column-visibility toggles by layering Tanstack's getVisibilityState-style APIs.