DataTable
DataTable displays tabular data with sorting, pagination, selection, and editable cells.
Source
LLM docs
Package
Examples
9
Properties
109
Playground
Basics
Define columns, feed the dataset, and let
DataTable handle search, sorting, and pagination out of the box.Loading demo…
import { useState } from 'react';
import { DataTable } from '@plocks/ui';
import type { DataTableColumn, DataTablePagination, DataTableSort } from '@plocks/ui';
import { people, type Person } from '../data';
const columns: DataTableColumn<Person>[] = [
{ key: 'name', header: 'Name', accessor: 'name', sortable: true },
{ key: 'email', header: 'Email', accessor: 'email', sortable: true, minWidth: 200 },
{ key: 'title', header: 'Role', accessor: 'title', sortable: true },
{ key: 'department', header: 'Department', accessor: 'department', sortable: true, minWidth: 160 },
];
export function Demo() {
const [sortBy, setSortBy] = useState<DataTableSort[]>([]);
const [pagination, setPagination] = useState<DataTablePagination>({
page: 1,
pageSize: 5,
total: people.length,
});
return (
<DataTable
data={people}
columns={columns}
sortBy={sortBy}
onSortChange={setSortBy}
pagination={pagination}
onPaginationChange={setPagination}
searchable
searchPlaceholder="Search teammates"
/>
);
}Column Filters
Mark columns
filterable and set filterType to pick the control: an input for text/number/date, a dropdown for select/boolean (options auto-derived from the data when filterOptions is omitted). showColumnFilters renders those controls as a persistent row under the headers; omit it to keep them in each header's filter menu.Loading demo…
import { useState } from 'react';
import { DataTable } from '@plocks/ui';
import type { DataTableColumn, DataTableFilter } from '@plocks/ui';
import { departmentFilterOptions, people, statusFilterOptions, type Person } from '../data';
const columns: DataTableColumn<Person>[] = [
// text → inline text input
{ key: 'name', header: 'Name', accessor: 'name', sortable: true, filterable: true, filterType: 'text' },
// select with explicit options → dropdown
{
key: 'department',
header: 'Department',
accessor: 'department',
sortable: true,
filterable: true,
filterType: 'select',
filterOptions: departmentFilterOptions,
},
{
key: 'status',
header: 'Status',
accessor: 'status',
sortable: true,
filterable: true,
filterType: 'select',
filterOptions: statusFilterOptions,
},
// boolean → Yes / No / All dropdown
{ key: 'remote', header: 'Remote', accessor: 'remote', filterable: true, filterType: 'boolean', cell: (v) => (v ? 'Yes' : 'No') },
// number → inline numeric input
{
key: 'salary',
header: 'Salary',
accessor: 'salary',
sortable: true,
filterable: true,
filterType: 'number',
dataType: 'currency',
align: 'right',
},
];
export function Demo() {
const [filters, setFilters] = useState<DataTableFilter[]>([]);
return (
<DataTable
data={people}
columns={columns}
filters={filters}
onFilterChange={setFilters}
showColumnFilters
searchable={false}
/>
);
}Variants
Compare the default, striped, and bordered table treatments using the same rows.
Loading demo…
import { Column, DataTable, Text } from '@plocks/ui';
const rows = [
{ id: 1, name: 'Avery', role: 'Designer' },
{ id: 2, name: 'Jordan', role: 'Engineer' },
];
const columns = [
{ key: 'name', header: 'Name', accessor: 'name' as const },
{ key: 'role', header: 'Role', accessor: 'role' as const },
];
const variants = ['default', 'striped', 'bordered'] as const;
export function Demo() {
return (
<Column gap="lg" fullWidth>
{variants.map(variant => (
<Column key={variant} gap="xs" fullWidth>
<Text fw="semibold">{variant}</Text>
<DataTable data={rows} columns={columns} variant={variant} />
</Column>
))}
</Column>
);
}Row Selection
Set
selectable and wire selectedRows / onSelectionChange to track checked rows. Pass a stable getRowId so selection survives sorting and paging; the table requires it for selection.Loading demo…
import { useState } from 'react';
import { Block, DataTable, Text } from '@plocks/ui';
import type { DataTableColumn, DataTablePagination } from '@plocks/ui';
import { people, type Person } from '../data';
const columns: DataTableColumn<Person>[] = [
{ key: 'name', header: 'Name', accessor: 'name', sortable: true },
{ key: 'email', header: 'Email', accessor: 'email', sortable: true, minWidth: 200 },
{ key: 'role', header: 'Role', accessor: 'role', sortable: true },
];
export function Demo() {
const [pagination, setPagination] = useState<DataTablePagination>({
page: 1,
pageSize: 5,
total: people.length,
});
const [selectedRows, setSelectedRows] = useState<(string | number)[]>([]);
return (
<Block fullWidth>
<Text size="sm" c={selectedRows.length ? 'primary' : 'muted'}>
{selectedRows.length ? `${selectedRows.length} selected` : 'No rows selected'}
</Text>
<DataTable
data={people}
columns={columns}
pagination={pagination}
onPaginationChange={setPagination}
selectable
selectedRows={selectedRows}
onSelectionChange={setSelectedRows}
getRowId={(row) => row.id}
searchable={false}
/>
</Block>
);
}Rich Cells
Combine avatars, chips, and status cues inside custom
cell renderers to create a readable, on-brand table.Loading demo…
import { Avatar, Chip, DataTable, Text } from '@plocks/ui';
import type { DataTableColumn } from '@plocks/ui';
import { people, type Person } from '../data';
const rows = people.slice(0, 5);
const columns: DataTableColumn<Person>[] = [
{
key: 'name',
header: 'Teammate',
accessor: 'name',
sortable: true,
cell: (_value, row) => (
<Avatar
size="sm"
fallback={row.name
.split(' ')
.map((part) => part[0])
.join('')}
label={<Text fw="semibold">{row.name}</Text>}
description={<Text variant="small" c="muted">{row.title}</Text>}
gap={8}
/>
),
},
{
key: 'team',
header: 'Team',
accessor: 'team',
sortable: true,
cell: (value) => (
<Chip size="xs" color="primary" variant="light">
{value}
</Chip>
),
},
{
key: 'status',
header: 'Status',
accessor: 'status',
sortable: true,
cell: (value: Person['status']) => (
<Text
c={value === 'inactive' ? 'error' : value === 'pending' ? 'warning' : 'success'}
fw="semibold"
>
{value.charAt(0).toUpperCase() + value.slice(1)}
</Text>
),
},
{
key: 'performance',
header: 'Score',
accessor: 'performance',
sortable: true,
align: 'right',
cell: (value) => <Text fw="semibold">{value.toFixed(1)}</Text>,
},
];
export function Demo() {
return (
<DataTable
data={rows}
columns={columns}
density="comfortable"
variant="striped"
searchable={false}
/>
);
}Expandable Rows
Provide
expandedRows, update them via onExpandedRowsChange, and use expandableRowRender to reveal supporting context.Loading demo…
import { useState } from 'react';
import { Block, DataTable, Text } from '@plocks/ui';
import type { DataTableColumn } from '@plocks/ui';
import { projects, type Project } from '../data';
const columns: DataTableColumn<Project>[] = [
{ key: 'name', header: 'Project', accessor: 'name', sortable: true },
{ key: 'owner', header: 'Owner', accessor: 'owner', sortable: true },
{
key: 'budget',
header: 'Budget',
accessor: 'budget',
align: 'right',
sortable: true,
dataType: 'currency',
},
];
export function Demo() {
const [expandedRows, setExpandedRows] = useState<(string | number)[]>([projects[0].id]);
return (
<DataTable
data={projects}
columns={columns}
getRowId={(row) => row.id}
expandedRows={expandedRows}
onExpandedRowsChange={setExpandedRows}
expandableRowRender={(project) => (
<Block p="md">
<Text c="muted">{project.summary}</Text>
</Block>
)}
searchable={false}
/>
);
}Grouping & Totals
Set
groupBy to a column key to group rows under collapsible group-header rows. Add aggregate (sum, avg, min, max, count, or a function) to any column to show its per-group total in the group header, and set showFooterTotals for a grand-total footer row aligned to the same columns. Grouping spans all filtered rows, so client pagination is bypassed while it is active.Loading demo…
import { DataTable } from '@plocks/ui';
import type { DataTableColumn } from '@plocks/ui';
import { sales as rows, type Sale } from '../data';
const columns: DataTableColumn<Sale>[] = [
{ key: 'region', header: 'Region', accessor: 'region' },
{ key: 'rep', header: 'Rep', accessor: 'rep', aggregate: 'count' },
{ key: 'product', header: 'Product', accessor: 'product' },
{ key: 'units', header: 'Units', accessor: 'units', dataType: 'number', align: 'right', aggregate: 'sum' },
{ key: 'revenue', header: 'Revenue', accessor: 'revenue', dataType: 'currency', align: 'right', aggregate: 'sum' },
];
export function Demo() {
return (
<DataTable
data={rows}
columns={columns}
groupBy="region"
showFooterTotals
footerLabel="All regions"
searchable={false}
showColumnVisibilityManager={false}
/>
);
}Fixed height & sticky columns
Pass a fixed
h to cap the table's size — the header row stays pinned while the body scrolls, so a long list fits a constrained panel without paginating. Pin columns to the edges with sticky: 'left' or sticky: 'right' so they stay put while the rest scroll horizontally; give each pinned column an explicit numeric width so its frozen offset lines up. Sticky positioning is web-only (a no-op on native).Loading demo…
import { DataTable } from '@plocks/ui';
import type { DataTableColumn } from '@plocks/ui';
type Server = {
id: number;
host: string;
region: string;
cpu: string;
memory: string;
uptime: string;
status: 'healthy' | 'degraded' | 'offline';
};
const REGIONS = ['us-east-1', 'us-west-2', 'eu-west-1', 'ap-south-1'];
const STATUSES: Server['status'][] = ['healthy', 'degraded', 'offline'];
const rows: Server[] = Array.from({ length: 40 }, (_, i) => ({
id: i + 1,
host: `node-${String(i + 1).padStart(2, '0')}.cluster.internal`,
region: REGIONS[i % REGIONS.length],
cpu: `${((i * 7) % 90) + 5}%`,
memory: `${((i * 13) % 80) + 10}%`,
uptime: `${(i % 30) + 1}d`,
status: STATUSES[i % STATUSES.length],
}));
const columns: DataTableColumn<Server>[] = [
// Pinned left, so the host stays visible while the rest scroll horizontally.
{ key: 'host', header: 'Host', accessor: 'host', sticky: 'left', width: 240, sortable: true },
{ key: 'region', header: 'Region', accessor: 'region', width: 160, sortable: true },
{ key: 'cpu', header: 'CPU', accessor: 'cpu', width: 120, align: 'right', sortable: true },
{ key: 'memory', header: 'Memory', accessor: 'memory', width: 120, align: 'right', sortable: true },
{ key: 'uptime', header: 'Uptime', accessor: 'uptime', width: 120, align: 'right' },
{ key: 'status', header: 'Status', accessor: 'status', sticky: 'right', width: 140, sortable: true },
];
export function Demo() {
return (
<DataTable
data={rows}
columns={columns}
getRowId={(row) => row.id}
h={320}
fullWidth={false}
searchable={false}
/>
);
}Server-side pagination
Set
manualPagination when the data comes from a paginated API. The data prop is treated as the already-fetched current page — the table does no client-side slicing, filtering, or sorting — and pagination.total drives the page count and "X-Y of N" summary. The sort, filter, search, and page controls still fire their callbacks (onSortChange, onFilterChange, onSearchChange, onPaginationChange) so you can refetch. Pair it with loading to show the skeleton during each fetch.Loading demo…
import { useEffect, useState } from 'react';
import { DataTable } from '@plocks/ui';
import type { DataTableColumn, DataTablePagination, DataTableSort } from '@plocks/ui';
type Order = {
id: number;
customer: string;
product: string;
amount: number;
};
// Pretend this table lives on a server; the component only ever sees one page.
const DB: Order[] = Array.from({ length: 137 }, (_, i) => ({
id: i + 1,
customer: `Customer ${String(i + 1).padStart(3, '0')}`,
product: ['Starter', 'Pro', 'Team', 'Enterprise'][i % 4],
amount: Math.round(((i * 37) % 900) + 100),
}));
// Simulate an API endpoint: GET /orders?page&pageSize&sort
function fetchOrders(
page: number,
pageSize: number,
sort?: DataTableSort
): Promise<{ rows: Order[]; total: number }> {
return new Promise((resolve) => {
setTimeout(() => {
const sorted = [...DB];
if (sort?.direction) {
sorted.sort((a, b) => {
const av = a[sort.column as keyof Order];
const bv = b[sort.column as keyof Order];
const cmp = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
return sort.direction === 'desc' ? -cmp : cmp;
});
}
const start = (page - 1) * pageSize;
resolve({ rows: sorted.slice(start, start + pageSize), total: DB.length });
}, 500);
});
}
const columns: DataTableColumn<Order>[] = [
{ key: 'id', header: 'Order', accessor: 'id', sortable: true, dataType: 'number' },
{ key: 'customer', header: 'Customer', accessor: 'customer', sortable: true },
{ key: 'product', header: 'Plan', accessor: 'product', sortable: true },
{ key: 'amount', header: 'Amount', accessor: 'amount', sortable: true, dataType: 'currency' },
];
export function Demo() {
const [rows, setRows] = useState<Order[]>([]);
const [loading, setLoading] = useState(true);
const [sortBy, setSortBy] = useState<DataTableSort[]>([]);
const [pagination, setPagination] = useState<DataTablePagination>({ page: 1, pageSize: 10, total: 0 });
// Refetch whenever the page, page size, or sort changes.
useEffect(() => {
let cancelled = false;
setLoading(true);
fetchOrders(pagination.page, pagination.pageSize, sortBy[0]).then((res) => {
if (cancelled) return;
setRows(res.rows);
setPagination((p) => (p.total === res.total ? p : { ...p, total: res.total }));
setLoading(false);
});
return () => {
cancelled = true;
};
}, [pagination.page, pagination.pageSize, sortBy]);
return (
<DataTable
data={rows}
columns={columns}
loading={loading}
manualPagination
pagination={pagination}
onPaginationChange={setPagination}
sortBy={sortBy}
onSortChange={setSortBy}
getRowId={(row) => row.id}
searchable={false}
/>
);
}React Native design for iOS, Android, and Web.
Quick Links
Resources