DataTable
A feature-rich data table built on TanStack Table with sorting, filtering, pagination, and row selection.
Column headers use aria-sort; the table carries a <caption> for screen readers; row checkboxes have aria-label.
Preview
tsx
import { type ColumnDef, DataTable } from '@arshad-shah/cynosure-react';
type User = { id: number; name: string; email: string; status: string };
const data: User[] = [
{ id: 1, name: 'Alice Martin', email: 'alice@example.com', status: 'Active' },
{ id: 2, name: 'Bob Chen', email: 'bob@example.com', status: 'Active' },
{ id: 3, name: 'Carol Smith', email: 'carol@example.com', status: 'Away' },
{ id: 4, name: 'Dan Kumar', email: 'dan@example.com', status: 'Inactive' },
{ id: 5, name: 'Eva Lopez', email: 'eva@example.com', status: 'Active' },
];
const columns: ColumnDef<User>[] = [
{ accessorKey: 'id', header: 'ID' },
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
{ accessorKey: 'status', header: 'Status' },
];
export default function Example() {
return <DataTable data={data} columns={columns} caption="Team members" />;
}
Variants
Section titled “Variants”Sortable
Section titled “Sortable”Enable column sorting by passing sortable. Click any column header to toggle ascending / descending order.
Preview
tsx
import { type ColumnDef, DataTable } from '@arshad-shah/cynosure-react';
type Product = { id: number; name: string; category: string; price: number };
const data: Product[] = [
{ id: 1, name: 'Wireless Headphones', category: 'Electronics', price: 89.99 },
{ id: 2, name: 'Leather Notebook', category: 'Stationery', price: 14.5 },
{ id: 3, name: 'USB-C Hub', category: 'Electronics', price: 45.0 },
{ id: 4, name: 'Desk Lamp', category: 'Office', price: 32.99 },
{ id: 5, name: 'Standing Mat', category: 'Office', price: 59.0 },
];
const columns: ColumnDef<Product>[] = [
{ accessorKey: 'id', header: 'ID' },
{ accessorKey: 'name', header: 'Product' },
{ accessorKey: 'category', header: 'Category' },
{
accessorKey: 'price',
header: 'Price',
cell: ({ getValue }) => `$${(getValue() as number).toFixed(2)}`,
},
];
export default function Example() {
return (
<DataTable
data={data}
columns={columns}
sortable
caption="Products — click a column header to sort"
/>
);
}
Filterable
Section titled “Filterable”Pass a filter object with a global string and onGlobalFilterChange handler. Use the toolbar prop to render a search input above the table.
Preview
tsx
'use client';
import { type ColumnDef, DataTable } from '@arshad-shah/cynosure-react';
import { useState } from 'react';
type Employee = { id: number; name: string; department: string; status: string };
const data: Employee[] = [
{ id: 1, name: 'Alice Martin', department: 'Engineering', status: 'Active' },
{ id: 2, name: 'Bob Chen', department: 'Design', status: 'Active' },
{ id: 3, name: 'Carol Smith', department: 'Product', status: 'Away' },
{ id: 4, name: 'Dan Kumar', department: 'Engineering', status: 'Inactive' },
{ id: 5, name: 'Eva Lopez', department: 'Marketing', status: 'Active' },
];
const columns: ColumnDef<Employee>[] = [
{ accessorKey: 'id', header: 'ID' },
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'department', header: 'Department' },
{ accessorKey: 'status', header: 'Status' },
];
export default function Example() {
const [search, setSearch] = useState('');
return (
<DataTable
data={data}
columns={columns}
filter={{ global: search, onGlobalFilterChange: setSearch }}
toolbar={
<input
type="search"
placeholder="Search employees…"
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{
padding: '6px 10px',
borderRadius: 6,
border: '1px solid var(--color-border)',
width: 220,
}}
/>
}
caption="Employees"
/>
);
}
Paginated
Section titled “Paginated”Pass pagination with a pageSize to split rows into pages. A Pagination control is rendered below the table automatically.
Preview
tsx
import { type ColumnDef, DataTable } from '@arshad-shah/cynosure-react';
type Order = { id: string; customer: string; total: number; status: string };
const CUSTOMERS = ['Alice', 'Bob', 'Carol', 'Dan', 'Eva', 'Frank'] as const;
const STATUSES = ['Shipped', 'Pending', 'Delivered', 'Cancelled'] as const;
const data: Order[] = Array.from({ length: 12 }, (_, i) => ({
id: `ORD-${String(i + 1).padStart(3, '0')}`,
customer: CUSTOMERS[i % CUSTOMERS.length] ?? 'Unknown',
total: Number(((i + 1) * 17.5).toFixed(2)),
status: STATUSES[i % STATUSES.length] ?? 'Pending',
}));
const columns: ColumnDef<Order>[] = [
{ accessorKey: 'id', header: 'Order' },
{ accessorKey: 'customer', header: 'Customer' },
{
accessorKey: 'total',
header: 'Total',
cell: ({ getValue }) => `$${(getValue() as number).toFixed(2)}`,
},
{ accessorKey: 'status', header: 'Status' },
];
export default function Example() {
return (
<DataTable
data={data}
columns={columns}
pagination={{ pageSize: 5 }}
caption="Orders — 5 per page"
/>
);
}
PropTypeDefaultDescription
data*
TData[]
—
Row data. Each row is rendered once unless paginated; identity tracked by `getRowId`.
columns*
ColumnDef<TData>[]
—
Column definitions from `@tanstack/react-table`. Each `ColumnDef` provides
an `accessorKey`/`accessorFn` that yields the cell value, a `header`, and
an optional `cell` renderer. When `selectable` is true an internal
`__select` column is prepended automatically.
sortable
boolean
false
Enable click-to-sort on each column that hasn't opted out. Holding shift
while clicking adds a secondary sort. Sorted columns expose
`aria-sort="ascending|descending|none"` for assistive tech.
selectable
boolean
false
Enable a leading checkbox column for row selection. Header checkbox
toggles the entire visible page.
onSelectionChange
((rows: TData[]) => void)
—
Fires with the live array of selected rows whenever selection changes.
pagination
boolean|DataTablePagination
—
`true` enables pagination with defaults; pass a {@link DataTablePagination}
to customise page size, initial index, or wire an external change
listener. When enabled, the pagination footer renders below the table.
filter
DataTableFilter
—
Controlled global filter — leave undefined to disable filtering.
emptyState
ReactNode
"No results."
Body rendered when `data.length === 0` (after filters applied).
loading
boolean
false
Render skeleton rows instead of data. Useful while waiting on async data.
loadingRows
number
6
Number of skeleton rows shown while `loading` is true.
tableVariant
"line"|"striped"|"grid"|"minimal"
"line"
Visual variant forwarded to the underlying {@link Table}.
tableSize
"sm"|"md"|"lg"
"md"
Size token forwarded to the underlying {@link Table}.
stickyHeader
boolean
—
Pin the table header to the scroll container's top edge while body scrolls.
toolbar
ReactNode
—
Optional toolbar rendered above the table (e.g. the search input).
getRowId
((row: TData, index: number) => string)
—
Custom row identifier — used as the React key and the selection map key.
caption
ReactNode
—
Caption rendered inside the `<caption>` element for screen readers.
ref
Ref<HTMLDivElement>
—
Accessibility
Section titled “Accessibility”- Sortable column headers carry
aria-sort="ascending",aria-sort="descending", oraria-sort="none"to communicate sort state to screen readers. - Row selection checkboxes have
aria-label="Select row"/aria-label="Select all rows on this page". - Provide a
captionprop to give the table a meaningful title that is read aloud by screen readers. - The empty-state row spans all columns and is announced as a single cell.
- The loading skeleton rows are visually distinguishable; no ARIA live region is emitted (add one in a wrapper if needed).
Recipes
Section titled “Recipes”- Use
selectable+onSelectionChangeto build bulk-action UIs. - Use
loading+loadingRowsto show skeleton rows while data is fetching. - Use
emptyStatewith a customReactNodefor a branded empty experience. - Set
tableVariant="striped"ortableSize="sm"to pass through to the underlyingTable. - Use
stickyHeaderwith a fixed-height scrollable wrapper to keep column headers visible. - Supply
getRowIdto use a stable business key (instead of row index) for row selection and re-renders.