Components

DataTable

DataTable is a feature-rich data table with built-in sorting, filtering, pagination, column visibility, and row selection.

DataTable is built on TanStack Table and should be preferred over the lower-level Table component for any use case involving dynamic data. Table is a dormant component for styled HTML table elements only.

() => {
const data = [
{ name: 'Alice', role: 'Engineer', department: 'Platform', active: true },
{ name: 'Bob', role: 'Designer', department: 'Product', active: true },
{ name: 'Charlie', role: 'Manager', department: 'Platform', active: false },
{ name: 'Diana', role: 'Analyst', department: 'Data', active: true },
{ name: 'Eve', role: 'Developer', department: 'Product', active: true },
]
const columns = [
{ key: 'name', label: 'Name', isRowHeader: true },
{ key: 'role', label: 'Role' },
{ key: 'department', label: 'Department' },
{
key: 'active',
label: 'Status',
renderCell: (value) => (
<Badge badgeColor={value ? 'green' : 'red'}>
{value ? 'Active' : 'Inactive'}
</Badge>
),
},
]
return (
<DataTable
data={data}
columns={columns}
caption="Team Members"
emptyMessage="No team members found"
/>
)
}

Properties

NameTypeDefaultDescriptionRequired?

Sorting

All columns are sortable by default — click a header to sort ascending, again for descending, again to clear. Disable sorting globally with enableSorting={false} or per-column with disableSorting: true.

Use sortingFn on a column to provide a custom comparator. Write it for ascending order; TanStack Table handles direction automatically. Use sortDescFirst: true to make the first click sort descending — useful for numeric columns where highest-first is more natural.

() => {
const [sorting, setSorting] = useState([{ id: 'name', desc: false }])
const data = [
{ name: 'Charlie', score: 92, department: 'Engineering' },
{ name: 'Alice', score: 88, department: 'Design' },
{ name: 'Bob', score: 95, department: 'Engineering' },
{ name: 'Diana', score: 79, department: 'Marketing' },
]
const columns = [
{ key: 'name', label: 'Name' },
{
key: 'score',
label: 'Score',
sortDescFirst: true,
renderCell: (value) => `${value}/100`,
},
{ key: 'department', label: 'Department', disableSorting: true },
]
return (
<DataTable
data={data}
columns={columns}
caption="Team Scores"
emptyMessage="No data"
state={{ sorting }}
onSortingChange={setSorting}
/>
)
}

Custom Cell Rendering

Use renderCell on a column to control how each cell renders. It receives the raw cell value and the full row object. Set isRowHeader: true to render a column's cells as <th scope="row"> — use this for the column that identifies each row.

() => {
const data = [
{ name: 'Alice', email: 'alice@example.com', score: 94 },
{ name: 'Bob', email: 'bob@example.com', score: 72 },
{ name: 'Charlie', email: 'charlie@example.com', score: 85 },
]
const columns = [
{ key: 'name', label: 'Name', isRowHeader: true },
{
key: 'email',
label: 'Email',
renderCell: (value) => (
<a href={`mailto:${value}`}>{value}</a>
),
},
{
key: 'score',
label: 'Score',
sortDescFirst: true,
renderCell: (value) => {
const color = value >= 90 ? 'green' : value >= 80 ? 'yellow' : 'red'
return <Badge badgeColor={color}>{value}/100</Badge>
},
},
]
return (
<DataTable
data={data}
columns={columns}
caption="User Scores"
emptyMessage="No users found"
/>
)
}

Simple Pagination (Show More / Show Less)

Use simplePaginationConfig to progressively reveal rows. Best for compact tables where all data is already loaded client-side.

() => {
const data = Array.from({ length: 20 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
department: ['Engineering', 'Design', 'Marketing', 'Sales'][i % 4],
}))
const columns = [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Name' },
{ key: 'department', label: 'Department' },
]
return (
<DataTable
data={data}
columns={columns}
caption="Users"
emptyMessage="No users"
simplePaginationConfig={{
initialVisibleRows: 5,
expandBy: 5,
showMoreText: 'Show More',
showLessText: 'Show Less',
displayShowAllButton: true,
showAllText: 'Show All',
}}
/>
)
}

Full Pagination

Use paginationConfig for page-based navigation with a page size selector. Sorting operates across the full dataset, not just the current page. simplePaginationConfig and paginationConfig are mutually exclusive.

() => {
const data = Array.from({ length: 50 }, (_, i) => ({
name: `User ${i + 1}`,
role: ['Engineer', 'Designer', 'Manager', 'Analyst'][i % 4],
score: 60 + (i % 40),
}))
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'role', label: 'Role' },
{ key: 'score', label: 'Score', sortDescFirst: true },
]
return (
<DataTable
data={data}
columns={columns}
caption="All Users"
emptyMessage="No users"
paginationConfig={{
initialPageSize: 10,
displayType: 'dropdown',
pageSizeOptions: [10, 25, 50],
}}
/>
)
}

Search / Filtering (Experimental)

Pass a string to _experimentalSearch to filter rows across all columns. This API is experimental and may change.

The default mode is "fuzzy" — it uses match-sorter for tolerant matching. Use _experimentalSearchFilterFn="includesString" for exact substring matching. When the search value changes, pagination resets to page 1. Exclude specific columns with disableGlobalFilter: true on the column definition.

() => {
const [search, setSearch] = useState('')
const data = [
{ name: 'Alice Chen', role: 'Engineer', location: 'Chicago' },
{ name: 'Bob Smith', role: 'Designer', location: 'New York' },
{ name: 'Charlie Park', role: 'Manager', location: 'Austin' },
{ name: 'Diana Lee', role: 'Analyst', location: 'Remote' },
{ name: 'Eve Johnson', role: 'Engineer', location: 'San Francisco' },
]
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'role', label: 'Role' },
{ key: 'location', label: 'Location' },
]
return (
<Box>
<Box mb={300}>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
</Box>
<DataTable
data={data}
columns={columns}
caption="Team Directory"
emptyMessage="No results found"
_experimentalSearch={search}
/>
</Box>
)
}

Column Visibility

Control which columns render using state.columnVisibility. Columns not listed default to visible.

() => {
const [columnVisibility, setColumnVisibility] = useState({ email: false })
const data = [
{ name: 'Alice', role: 'Engineer', email: 'alice@example.com', score: 94 },
{ name: 'Bob', role: 'Designer', email: 'bob@example.com', score: 81 },
{ name: 'Charlie', role: 'Manager', email: 'charlie@example.com', score: 76 },
]
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'role', label: 'Role' },
{ key: 'email', label: 'Email' },
{ key: 'score', label: 'Score' },
]
return (
<Box>
<Box mb={300}>
<Button
onClick={() =>
setColumnVisibility((prev) => ({ ...prev, email: !prev.email }))
}
>
{columnVisibility.email === false ? 'Show Email' : 'Hide Email'}
</Button>
</Box>
<DataTable
data={data}
columns={columns}
caption="Team Members"
emptyMessage="No data"
state={{ columnVisibility }}
onColumnVisibilityChange={setColumnVisibility}
/>
</Box>
)
}

Fixed Layout

Set fixedLayout to apply table-layout: fixed. This prevents columns from shifting when paginating or when cell content varies in length. Use size on each column to set widths in pixels; columns without a size share remaining space equally.

() => {
const data = [
{ name: 'Alice', role: 'Software Engineer', notes: 'Very long notes that would normally push the column wider than expected in a fluid layout.' },
{ name: 'Bob', role: 'Designer', notes: 'Short notes.' },
{ name: 'Charlie', role: 'Project Manager', notes: 'Medium length notes for this row.' },
]
const columns = [
{ key: 'name', label: 'Name', size: 120 },
{ key: 'role', label: 'Role', size: 180 },
{ key: 'notes', label: 'Notes' },
]
return (
<DataTable
data={data}
columns={columns}
caption="Team with Fixed Layout"
emptyMessage="No data"
fixedLayout
/>
)
}

Server-Side Sorting and Pagination

For data loaded from a server, manage state externally and pass it via state. Set serverSideSorting={true} so the table does not sort rows client-side. Set state.pagination.totalRows to the full dataset count so the pagination component can calculate page counts correctly.

() => {
const [sorting, setSorting] = useState([])
const [pageIndex, setPageIndex] = useState(0)
const pageSize = 5
const allRows = Array.from({ length: 23 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
score: 100 - i,
}))
const sorted = [...allRows].sort((a, b) => {
if (!sorting.length) return 0
const { id, desc } = sorting[0]
const dir = desc ? -1 : 1
return a[id] > b[id] ? dir : a[id] < b[id] ? -dir : 0
})
const pageRows = sorted.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize)
const columns = [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Name' },
{ key: 'score', label: 'Score', sortDescFirst: true },
]
return (
<DataTable
data={pageRows}
columns={columns}
caption="Server-Side Table"
emptyMessage="No data"
serverSideSorting
state={{
sorting,
pagination: { pageIndex, pageSize, totalRows: allRows.length },
}}
onSortingChange={setSorting}
onPageIndexChange={setPageIndex}
paginationConfig={{
initialPageSize: pageSize,
displayType: 'dropdown',
pageSizeOptions: [5, 10, 25],
}}
/>
)
}

Custom Caption

Pass any data structure to caption and use renderCaption to control how it renders. This is useful for pairing the table title with badges or other metadata.

() => {
const data = [
{ name: 'Alice', role: 'Engineer', department: 'Platform' },
{ name: 'Bob', role: 'Designer', department: 'Product' },
{ name: 'Charlie', role: 'Manager', department: 'Platform' },
]
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'role', label: 'Role' },
{ key: 'department', label: 'Department' },
]
return (
<DataTable
data={data}
columns={columns}
caption={{ title: 'Team Members', count: 3 }}
renderCaption={(cap) => (
<Box display="flex" alignItems="center">
<Text mr={200} fontWeight="semibold">{cap.title}</Text>
<Badge>{cap.count}</Badge>
</Box>
)}
emptyMessage="No members"
/>
)
}

Accessibility

  • caption is required. It labels the table for screen readers. Use displayCaption={false} only when the table's purpose is already clear from context.
  • Use isRowHeader: true on the column that uniquely identifies each row (e.g. name or ID) — it renders as <th scope="row">.
  • Sort state is communicated via aria-sort on header cells.

Best Practices

  • Prefer DataTable over the dormant Table component for any dynamic data use case.
  • Always provide a meaningful caption even when displayCaption={false}.
  • Use fixedLayout when combining pagination with defined column widths to prevent layout shifts between pages.
  • Use simplePaginationConfig for short client-side lists; use paginationConfig for larger datasets or server-side pagination.
  • _experimentalSearch is experimental — its API may change. For server-side search, filter your data before passing it to DataTable.