Table
Display structured data with keyboard navigation, optional sorting, and row selection.
| Project | Status | Team | Budget |
|---|---|---|---|
| Website Redesign | Frontend Team | $12,500 | |
| Mobile App | Mobile Team | $8,750 | |
| API Integration | Backend Team | $5,200 | |
| Security Audit | Security Team | $2,100 |
Installation
pnpm dlx shadcn@4.18.0 add @siteplane/tableComplete the one-time setup first. Registry JSON lists source files and dependencies.
For this page’s examples, also install:
pnpm dlx shadcn@4.18.0 add @siteplane/badge @siteplane/button @siteplane/card @siteplane/menuIncludes react-aria-components. Import Selection and Key from that package; SortDescriptor is also exported by Siteplane UI's Table module.
Usage
import {
Table,
TableHeader,
TableHead,
TableBody,
TableRow,
TableCell,
} from "@/siteplane/components/table";
<Table aria-label="Projects">
<TableHeader>
<TableHead id="project" isRowHeader>
Project
</TableHead>
<TableHead id="team">Team</TableHead>
</TableHeader>
<TableBody>
<TableRow id="website-redesign" textValue="Website Redesign">
<TableCell>Website Redesign</TableCell>
<TableCell>Frontend Team</TableCell>
</TableRow>
</TableBody>
</Table>;Composition
Table
├── TableCaption (optional)
├── TableHeader
│ └── TableHead
├── TableBody
│ └── TableRow
│ └── TableCell
└── TableFooter (optional)
└── TableRow
└── TableCellTableHead is a direct child of TableHeader; do not insert a header TableRow. Use stable row and column IDs, mark an identifying column with isRowHeader, and provide a plain-text textValue for rows with rich content. These are React Aria collection parts.
Sorting
allowsSorting enables the column control. Your application handles onSortChange, updates sortDescriptor, and reorders the data.
| Project | Role | Team | Budget |
|---|---|---|---|
| Website Redesign | Owner | Frontend Team | $12,500 |
| Mobile App | Editor | Mobile Team | $8,750 |
| API Integration | Editor | Backend Team | $5,200 |
Use the appropriate comparison for each column: numbers for budgets, locale-aware text for names, and dates for date columns. Preserve row IDs when reordering.
Row selection
Selection is off by default. Use selectionMode="multiple" with selectedKeys and onSelectionChange, and set selectionTrigger="checkbox" so only TableSelectionCheckbox changes the selection; the header checkbox selects all available rows and shows a partial selection as indeterminate.
| Project | Role | Team | Budget | |
|---|---|---|---|---|
| Website Redesign | Owner | Frontend Team | $12,500 | |
| Mobile App | Editor | Mobile Team | $8,750 | |
| API Integration | Editor | Backend Team | $5,200 |
The check of a selected row draws in when the row is selected and out when it is not, the way the Checkbox check does, and so does the dash of a partial selection; both appear instantly under reduced motion. The check and the dash take the text color for accent fills, --sp-rw-fg-on-accent, which a light brand sets dark (Theming).
Selection can be a key set or "all"; handle both. For remote or paginated data, define the scope of “all” before bulk actions. With the default selectionTrigger="row", clicking a row also selects it. Use selectionMode="single" for one row and disallowEmptySelection to prevent clearing an existing selection.
Row actions
Place a Menu in the actions column for row-specific commands. Use onAction for whole-row activation and href for navigation.
| Project | Team | Actions |
|---|---|---|
| Website Redesign | Frontend Team | |
| Mobile App | Mobile Team | |
| API Integration | Backend Team |
Without selection, a click or Enter activates the row. When the table also allows selection, selectionBehavior decides what a click does. With toggle, the default, a click or Enter runs the row's action while no row is selected; once rows are selected, a click toggles the selection of the row instead. With replace, one click selects a row and a double click runs its action. With selectionTrigger="checkbox", only the checkboxes change the selection, so a click, a tap or Enter on the row runs its onAction whether rows are selected or not, and selectionBehavior does not apply. Space stays the selection key and runs no action. Links retain their navigation behavior; under selectionTrigger="checkbox", a row link navigates while no row is selected, so give such rows an onAction that navigates if they should also open while rows are selected.
A control inside a row keeps its own presses. A click on a menu trigger, a button or a link in a cell, and Enter or Space while it has focus, reach only that control: they neither run the row's action nor change the selection. This holds for React Aria controls, which stop their presses themselves, and for every other control, such as a Siteplane UI Menu trigger.
A root onRowAction(key) is also available. Use row-level onAction when the row should show Siteplane UI's interactive styling and pointer cursor. For rows in disabledKeys, disabledBehavior="all" prevents focus, selection and actions, while "selection" only prevents selection. inactive only dims a row; it does not restrict interaction.
Empty collection
Use TableBody renderEmptyState for an empty collection. The table keeps its column headings while the body displays the empty state. A select-all checkbox in the headings is unchecked and disabled while the table has no rows, also when keys of rows that a filter removed stay selected.
| Project | Team |
|---|---|
No projects found. | |
Table has no loading prop. Set aria-busy on the surrounding results region while loading, and show a distinct error/retry state for failed requests.
Caption and footer
Place TableCaption directly inside Table; it appears below the table and supplies its accessible name unless you provide aria-labelledby. Use TableFooter for summary rows and colSpan for labels that span columns; your application calculates the totals.
| Project | Role | Team | Budget |
|---|---|---|---|
| Website Redesign | Owner | Frontend Team | $12,500 |
| Mobile App | Editor | Mobile Team | $8,750 |
| API Integration | Editor | Backend Team | $5,200 |
| Total budget | $26,450 | ||
Card composition
CardFrame supplies the surrounding surface; Table owns its rows and horizontal scrolling. Inside CardFrameBleed, the first and last columns take the frame's inset, so the table lines up with the frame header and footer. Card is included in the example dependencies under Installation.
| Project | Role | Team | Budget |
|---|---|---|---|
| Website Redesign | Owner | Frontend Team | $12,500 |
| Mobile App | Editor | Mobile Team | $8,750 |
| API Integration | Editor | Backend Team | $5,200 |
API reference
Table
| Prop | Type | Default | Description |
|---|---|---|---|
hoverable | boolean | true | Enables header and row hover backgrounds. false disables them table-wide; selection, focus and actions remain available. |
size | sm | md | lg | xl | provider / lg | Shared table size preset. sm shortens the heading row and tightens the cell padding; lg and xl increase the row height. |
selection | row | checkbox | row | Which interaction updates the selection. With checkbox, only TableSelectionCheckbox selects and a press on a row runs its action. |
class | string | unset | Styles the table itself. A callback is not accepted. |
container | string | unset | Styles the outer scrolling container. |
selection | none | single | multiple | none | Whether rows can be selected. |
selection | toggle | replace | toggle | How row interaction updates selection and activates actions. Does not apply with selectionTrigger checkbox. |
selected | Selection | uncontrolled | Controlled key set, or all. |
default | Iterable<Key> | "all" | empty | Initial uncontrolled selection. |
on | (keys: Selection) => void | unset | Receives updated selected keys. |
disallow | boolean | false | Prevents clearing an existing selection. |
disabled | Iterable<Key> | empty | Rows whose interaction is restricted. |
disabled | all | selection | all | Disable all interaction or only selection for disabled rows. |
sort | SortDescriptor | unset | Active column and ascending/descending direction. |
on | (descriptor: SortDescriptor) => void | unset | Requests sorting; application reorders data. |
on | (key: Key) => void | unset | Root action callback. Use per-row onAction for Siteplane UI action styling. |
aria-label / aria-labelledby | string | unset / caption ID | Accessible table name. Supply a name or a direct TableCaption. |
children | ReactNode | unset | Header, body, optional direct caption and footer. |
TableHeader and TableHead
| Prop | Type | Default | Description |
|---|---|---|---|
Table | ReactNode or column render function / Iterable<T> | unset | Static columns or a dynamic column collection. |
Table | string | unset | Header styling. |
Table | start | center | end | start | Heading alignment: start is left, center is centered, end is right. |
Table | string | unset | Local column-heading styling. |
Table | Key | unset | Stable column identity; used by sortDescriptor. |
Table | boolean | false | Identifies the column used as the accessible row heading. |
Table | boolean | false | Enables sorting interaction and the direction icon. |
Table | ReactNode | render function | unset | Heading content; render function receives column state. |
TableHeader creates the header row internally. TableHead retains React Aria
Column's collection and column sizing props; no resize handle or resize UI is
supplied by this wrapper.
TableBody and TableFooter
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | (item: T) => ReactElement | unset | Rows, or a render function for items. |
items | Iterable<T> | unset | Dynamic row collection. |
dependencies | ReadonlyArray<unknown> | unset | External values that invalidate a cached dynamic collection. |
class | string | unset | Styles the body or footer. |
Table | (state) => ReactNode | unset | Content when the body has no rows; not a footer prop. |
TableRow
| Prop | Type | Default | Description |
|---|---|---|---|
inactive | boolean | false | Dims the row only; does not disable it. |
hoverable | boolean | true | Enables the ordinary hover fill. Action/focus states are separate. |
class | string | unset | Local row styling. |
id | Key | unset | Stable row key for selection and collection identity. |
text | string | unset | Plain-text row label for typeahead and accessibility. |
href | string | unset | Makes the row a link. Supports inherited target, rel and router options. |
on | () => void | unset | Row action callback; enables interactive presentation. |
is | boolean | false | Disables this row according to disabledBehavior. |
disabled | all | selection | inherited | Overrides the table disabled behavior for this row. |
children / columns | ReactNode or cell render function / Iterable<T> | unset | Static or dynamic cells. |
TableCell
| Prop | Type | Default | Description |
|---|---|---|---|
align | start | center | end | start | Cell text alignment. |
class | string | unset | Local cell styling. |
col | number | 1 | Number of columns occupied by this cell. |
children | ReactNode | render function | unset | Cell content, optionally based on React Aria cell state. |
TableSelectionCheckbox
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | unset | Local checkbox styling. |
is | boolean | table context | Optional explicit disabled state; normally inherited from selection context. |
aria-label | string | table context | Accessible label; normally provided by the table. |
Place it inside a selection TableHead for select-all (multiple selection) or a
TableCell for a row. Siteplane UI fixes slot="selection" and supplies the
check/indeterminate indicator. children and slot are intentionally not
accepted. Other inherited Checkbox props are available, but do not detach its
selected state from the table.
TableCaption and exported types
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Content extracted by the direct parent Table; not a standalone native caption. |
class | string | unset | Styles the caption below the scrolling table. |
Types exported by the Table module: TableProps, TableCaptionProps, TableHeaderProps<T>, TableHeadProps, TableBodyProps<T>, TableFooterProps<T>, TableRowProps<T>, TableCellProps, TableSelectionCheckboxProps, TableAlign, TableSelectionTrigger, and SortDescriptor. Import Selection and Key from react-aria-components when needed.
Inherited contracts and boundaries
Siteplane UI's className props are strings, even where React Aria accepts callbacks. The --table-edge-padding custom property sets the inline padding of the first and last columns; CardFrameBleed sets it to the frame inset, and outside a frame the outer columns keep the regular cell padding. Other inherited collection/render props remain available. See the official Table API for Table, TableHeader, Column, TableBody, TableFooter, Row and Cell, and Checkbox API for the underlying selection control. The installed source defines the exact version and overrides used here.
Pagination, filtering, remote loading and budget calculations belong to your application. Advanced React Aria composition such as drag-and-drop or resizable columns needs its additional hooks/parts; Siteplane UI Table does not provide dedicated controls for those flows. Do not expect a sort arrow to sort data automatically, a status badge to disable a row, or inactive to prevent an action.
Usage notes
Give the table an accessible name and at least one identifying row-header column. Arrow keys move through the grid, Home/End move within a row, and Ctrl+Home/Ctrl+End reach the beginning/end of the collection. Space selects a selectable row; with selectionTrigger="checkbox", it selects only on the row's checkbox. Enter activates an actionable row. Selection behavior affects pointer activation as described above. Preserve the table's focus indicators and checkbox labels.
The table fills the width of its container, and cells don't wrap. When the columns need more room than the container provides, the table scrolls horizontally inside its own container and the page itself doesn't overflow; on mobile, this horizontal scrolling is expected. Do not squeeze all columns into unreadable widths. If you customize the table width or wrapping through className, check real content at narrow widths.