Overview
Table is a real ARIA grid (role="grid") — arrow keys move focus between cells, columns can be sortable, and rows can be selectable, all handled by React Aria's table hooks. Composing it uses TableHeader/TableColumn/TableBody/TableRow/TableCell as data — read to build the table's collection — rather than plain <thead>/<tbody> JSX. Table renders the real <table> markup itself from that collection.
This is a deliberate trade: the authoring model is less "just markup" than most of the rest of Chassis React, in exchange for real sorting, selection, and keyboard grid navigation that would otherwise need to be hand-rolled.
If a table needs none of that, and especially if it's rendered from a React Server Component, use StaticTable instead. It takes the same styling props, renders plain markup, and ships no JavaScript.
| First name | Last name | Username |
|---|
| Mark | Otto | @mdo |
| Jacob | Thornton | @fat |
| Larry | Bird | @twitter |
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@chassis-ui/react'
export const Example = () => (
<Table aria-label="Users">
<TableHeader>
<TableColumn key="firstName">First name</TableColumn>
<TableColumn key="lastName">Last name</TableColumn>
<TableColumn key="handle">Username</TableColumn>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Mark</TableCell>
<TableCell>Otto</TableCell>
<TableCell>@mdo</TableCell>
</TableRow>
<TableRow>
<TableCell>Jacob</TableCell>
<TableCell>Thornton</TableCell>
<TableCell>@fat</TableCell>
</TableRow>
<TableRow>
<TableCell>Larry</TableCell>
<TableCell>Bird</TableCell>
<TableCell>@twitter</TableCell>
</TableRow>
</TableBody>
</Table>
)
Dynamic columns and rows
Both TableHeader and TableBody also accept a columns/items array plus a render function, for columns and rows built from data rather than written out by hand.
| First name | Last name | Username |
|---|
| Mark | Otto | @mdo |
| Jacob | Thornton | @fat |
| Larry | Bird | @twitter |
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@chassis-ui/react'
const columns = [
{ id: 'firstName', name: 'First name' },
{ id: 'lastName', name: 'Last name' },
{ id: 'handle', name: 'Username' }
]
const rows = [
{ id: 1, firstName: 'Mark', lastName: 'Otto', handle: '@mdo' },
{ id: 2, firstName: 'Jacob', lastName: 'Thornton', handle: '@fat' },
{ id: 3, firstName: 'Larry', lastName: 'Bird', handle: '@twitter' }
]
export const Example = () => (
<Table aria-label="Users">
<TableHeader columns={columns}>
{(column) => <TableColumn key={column.id}>{column.name}</TableColumn>}
</TableHeader>
<TableBody items={rows}>
{(row) => (
<TableRow key={row.id}>
{(columnKey) => <TableCell>{row[columnKey as keyof typeof row]}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
)
Sorting
Add allowsSorting to a TableColumn to make it clickable (and keyboard-activatable). Table doesn't sort the data itself — it reports the requested column/direction via onSortChange, so pair it with sortDescriptor and sort the array in that handler, as shown below.
| Name▲ | Role⇅ |
|---|
| Ashley Grant | Product |
| Jacob Thornton | Designer |
| Larry Bird | Engineer |
| Mark Otto | Engineer |
import { useMemo, useState } from 'react'
import type { SortDescriptor } from 'react-stately'
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@chassis-ui/react'
const rows = [
{ id: 1, name: 'Mark Otto', role: 'Engineer' },
{ id: 2, name: 'Jacob Thornton', role: 'Designer' },
{ id: 3, name: 'Larry Bird', role: 'Engineer' },
{ id: 4, name: 'Ashley Grant', role: 'Product' }
]
export const Example = () => {
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
column: 'name',
direction: 'ascending'
})
const sortedRows = useMemo(() => {
const key = sortDescriptor.column as keyof (typeof rows)[number]
const sorted = [...rows].sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0))
return sortDescriptor.direction === 'descending' ? sorted.reverse() : sorted
}, [sortDescriptor])
return (
<Table aria-label="Team" onSortChange={setSortDescriptor} sortDescriptor={sortDescriptor}>
<TableHeader>
<TableColumn key="name" allowsSorting>
Name
</TableColumn>
<TableColumn key="role" allowsSorting>
Role
</TableColumn>
</TableHeader>
<TableBody items={sortedRows}>
{(row) => (
<TableRow key={row.id}>
{(columnKey) => <TableCell>{row[columnKey as keyof typeof row]}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
)
}
Selection
Set selectionMode="multiple" for checkbox-based row selection (including a select-all checkbox in the header), or selectionMode="single" to select a row by clicking anywhere in it. Control the selection with selectedKeys/onSelectionChange — a selected row gets the same .active highlight the table's active variant applies.
import { useState } from 'react'
import type { Selection } from 'react-stately'
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@chassis-ui/react'
const rows = [
{ id: 1, name: 'Mark Otto', role: 'Engineer' },
{ id: 2, name: 'Jacob Thornton', role: 'Designer' },
{ id: 3, name: 'Larry Bird', role: 'Engineer' }
]
export const Example = () => {
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set())
return (
<Table
aria-label="Team"
onSelectionChange={setSelectedKeys}
selectedKeys={selectedKeys}
selectionMode="multiple"
>
<TableHeader>
<TableColumn key="name">Name</TableColumn>
<TableColumn key="role">Role</TableColumn>
</TableHeader>
<TableBody items={rows}>
{(row) => (
<TableRow key={row.id}>
{(columnKey) => <TableCell>{row[columnKey as keyof typeof row]}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
)
}
Keyboard navigation
Once a cell has focus, arrow keys move focus across the grid — no extra setup required. Typeahead, Home/End, and Page Up/Down navigation come from the same hooks.
Variants
bordered, borderless, hover, sm, striped, and color all apply to the <table> element itself, the same as any other table markup.
| Name | Role |
|---|
| Mark Otto | Engineer |
| Jacob Thornton | Designer |
| Larry Bird | Engineer |
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@chassis-ui/react'
const rows = [
{ id: 1, name: 'Mark Otto', role: 'Engineer' },
{ id: 2, name: 'Jacob Thornton', role: 'Designer' },
{ id: 3, name: 'Larry Bird', role: 'Engineer' }
]
export const Example = () => (
<Table aria-label="Team" bordered hover sm striped>
<TableHeader>
<TableColumn key="name">Name</TableColumn>
<TableColumn key="role">Role</TableColumn>
</TableHeader>
<TableBody items={rows}>
{(row) => (
<TableRow key={row.id}>
{(columnKey) => <TableCell>{row[columnKey as keyof typeof row]}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
)
Pass caption for a <caption> above the table. Pass footer for a <tfoot> below it — footer content is rendered as plain markup, not part of the keyboard-navigable grid, since totals/summary rows don't need to participate in cell-by-cell navigation.
Recent invoices| Name | Amount |
|---|
| Mark Otto | $120 |
| Jacob Thornton | $80 |
| Total | $200 |
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@chassis-ui/react'
const rows = [
{ id: 1, name: 'Mark Otto', amount: '$120' },
{ id: 2, name: 'Jacob Thornton', amount: '$80' }
]
export const Example = () => (
<Table
aria-label="Invoices"
caption="Recent invoices"
footer={
<tr>
<td>Total</td>
<td>$200</td>
</tr>
}
>
<TableHeader>
<TableColumn key="name">Name</TableColumn>
<TableColumn key="amount">Amount</TableColumn>
</TableHeader>
<TableBody items={rows}>
{(row) => (
<TableRow key={row.id}>
{(columnKey) => <TableCell>{row[columnKey as keyof typeof row]}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
)
Responsive tables
Wrap in a responsive container with the responsive prop — true for all viewports, or a breakpoint name to scroll only below it.
| Column 1 | Column 2 | Column 3 | Column 4 | Column 5 | Column 6 | Column 7 |
|---|
| Row 1, Column 1 | Row 1, Column 2 | Row 1, Column 3 | Row 1, Column 4 | Row 1, Column 5 | Row 1, Column 6 | Row 1, Column 7 |
| Row 2, Column 1 | Row 2, Column 2 | Row 2, Column 3 | Row 2, Column 4 | Row 2, Column 5 | Row 2, Column 6 | Row 2, Column 7 |
| Row 3, Column 1 | Row 3, Column 2 | Row 3, Column 3 | Row 3, Column 4 | Row 3, Column 5 | Row 3, Column 6 | Row 3, Column 7 |
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@chassis-ui/react'
const columns = ['Column 1', 'Column 2', 'Column 3', 'Column 4', 'Column 5', 'Column 6', 'Column 7']
const rows = [1, 2, 3]
export const Example = () => (
<Table aria-label="Wide data" responsive>
<TableHeader>
{columns.map((col) => (
<TableColumn key={col}>{col}</TableColumn>
))}
</TableHeader>
<TableBody items={rows.map((id) => ({ id }))}>
{(row) => (
<TableRow key={row.id}>
{(columnKey) => <TableCell>{`Row ${row.id}, ${columnKey}`}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
)
Stacked tables
For tables with too many columns to read comfortably even with horizontal scrolling, set stacked to convert rows into stacked label/value blocks below a container width — true always stacks, or pass a breakpoint name to stack only below it. Resize the panel below to see it switch.
Each TableColumn's text becomes the label shown before that column's value; set textValue on a column explicitly when its header isn't plain text (an icon, a Tooltip, etc.).
| Name | Role | Email | Status |
|---|
| Alice Martin | Admin | alice@example.com | Active |
| Bob Chen | Editor | bob@example.com | Active |
| Carol White | Viewer | carol@example.com | Inactive |
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@chassis-ui/react'
const rows = [
{ id: 1, name: 'Alice Martin', role: 'Admin', email: 'alice@example.com', status: 'Active' },
{ id: 2, name: 'Bob Chen', role: 'Editor', email: 'bob@example.com', status: 'Active' },
{ id: 3, name: 'Carol White', role: 'Viewer', email: 'carol@example.com', status: 'Inactive' }
]
export const Example = () => (
<Table aria-label="Users" stacked="md">
<TableHeader>
<TableColumn key="name">Name</TableColumn>
<TableColumn key="role">Role</TableColumn>
<TableColumn key="email">Email</TableColumn>
<TableColumn key="status">Status</TableColumn>
</TableHeader>
<TableBody items={rows}>
{(row) => (
<TableRow key={row.id}>
{(columnKey) => <TableCell>{row[columnKey as keyof typeof row]}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
)
Scope
- No virtualization. Every row renders directly — there's no windowing for very lg datasets. React Aria doesn't ship a public general-purpose virtualizer for Table (its internal one isn't meant for outside use), and layering in a separate library (e.g.
@tanstack/react-virtual) is a substantial enough change — fixed/measured row heights, a scroll container, and interaction with React Aria's own keyboard navigation and focus management — that it's deferred rather than bundled into this first pass. Most lg datasets are better served by server-side pagination anyway.
- No column resizing or reordering, no nested/expandable rows. React Aria's table hooks support these, but they're not wired up here.
API
Table
| Prop | Type | Default |
|---|
align? | "bottom" | "middle" | "top" | — |
| Set the vertical alignment of cell content. |
aria-label? | string | — |
| An accessible label for the table, used when there's no visible heading. |
aria-labelledby? | string | — |
| Identifies a visible heading element for the table. |
bordered? | boolean | — |
| Add borders on all sides of the table and cells. |
borderless? | boolean | — |
| Remove borders on all sides of the table and cells. |
caption? | ReactNode | — |
| Content shown above the table, functioning as a heading for it. |
children | [ReactElement<TableHeaderProps<T> & { className?: string | undefined; }, string | JSXElementConstructor<any>>, ReactElement<...>] | — |
A TableHeader and a TableBody, each built from TableColumn/TableRow/
TableCell — read as data to build the table's collection. Not rendered directly. |
className? | string | — |
| A string of all className you want applied to the component. |
color? | ContextColor | — |
| Sets the color of the component. |
disabledKeys? | Iterable<Key> | — |
| A list of row keys to disable. Disabled rows cannot be selected, focused, or interacted with. |
footer? | ReactNode | — |
Content shown below the table in a <tfoot>, e.g. a totals row. Rendered as plain markup —
not part of the keyboard-navigable grid. |
hover? | boolean | — |
| Enable a hover state on table rows. |
id? | string | — |
id forwarded to the table element. |
onSelectionChange? | ((keys: Selection) => void) | — |
| Handler that is called when the selection changes. |
onSortChange? | ((descriptor: SortDescriptor) => void) | — |
| Handler that is called when a column is sorted. |
ref? | Ref<HTMLTableElement> | — |
|
responsive? | boolean | "sm" | "md" | "lg" | "xl" | "2xl" | — |
| Make any table responsive across all viewports or pick a maximum breakpoint. |
selectedKeys? | Selection | — |
| The currently selected row keys (controlled). |
selectionMode? | "none" | "single" | "multiple" | none |
| The type of selection that is allowed. |
sm? | boolean | — |
| Make table more compact by cutting all cell padding. |
sortDescriptor? | SortDescriptor | — |
| The current sort column and direction. |
stacked? | boolean | "sm" | "md" | "lg" | "xl" | "2xl" | — |
Convert rows into stacked label/value blocks below a container width, for tables with too
many columns to read comfortably even with horizontal scrolling. true always stacks; a
breakpoint name stacks only below it. Implies responsive when responsive isn't set
separately, since stacking needs the same .table-responsive container-query ancestor.
Labels come from each TableColumn's text (or its textValue, for non-text headers). |
striped? | boolean | — |
| Add zebra-striping to table rows. |
| Prop | Type | Default |
|---|
children | ReactElement<unknown, string | JSXElementConstructor<any>> | ReactElement<unknown, string | JSXElementConstructor<any>>[] | ((column: T) => ReactElement<...>) | — |
TableColumn elements, or a render function paired with columns for dynamic column
generation. |
className? | string | — |
A string of all className you want applied to the <thead> element. |
columns? | readonly T[] | — |
A list of column data objects, rendered via the function form of children. |
TableColumn
| Prop | Type | Default |
|---|
allowsSorting? | boolean | — |
| Whether the column allows sorting. Adds a sort indicator and makes the header
clickable/keyboard-activatable. |
children | ReactNode | — |
| Rendered contents of the column header. |
className? | string | — |
| A string of all className you want applied to the column header. |
textValue? | string | — |
A string representation of the column header, used for accessibility announcements and,
when Table's stacked prop is set, as the label shown before each row's value for this
column. Defaults to children when it's a plain string — set this explicitly when the
header contains anything else (an icon, a Tooltip, etc.). |
TableBody
| Prop | Type | Default |
|---|
children | ReactElement<unknown, string | JSXElementConstructor<any>> | ReactElement<unknown, string | JSXElementConstructor<any>>[] | ((item: T) => ReactElement<...>) | — |
TableRow elements, or a render function paired with items for dynamic row generation. |
className? | string | — |
A string of all className you want applied to the <tbody> element. |
items? | Iterable<T> | — |
A list of row data objects, rendered via the function form of children. |
TableRow
| Prop | Type | Default |
|---|
children | ReactElement<unknown, string | JSXElementConstructor<any>> | ReactElement<unknown, string | JSXElementConstructor<any>>[] | ((columnKey: Key) => ReactElement<...>) | — |
TableCell elements, or a render function called once per column with that column's key —
required when the row's parent TableBody uses the items/render-function form. |
className? | string | — |
| A string of all className you want applied to the row. |
textValue? | string | — |
| A string representation of the row's contents, used for typeahead. |
TableCell
| Prop | Type | Default |
|---|
children | ReactNode | — |
| The contents of the cell. |
className? | string | — |
| A string of all className you want applied to the cell. |
colSpan? | number | — |
| Indicates how many columns the cell spans. |
textValue? | string | — |
| A string representation of the cell's contents, used for typeahead. |