TableView
Tables are containers for displaying information. They allow users to quickly scan, sort, compare, and take action on large amounts of data.
density
Content
TableView
follows the Collection Components API, accepting both static and dynamic collections.
In this example, both the columns and the rows are provided to the table via a render function, enabling the user to hide and show columns and add additional rows.
import {TableView, TableHeader, Column, TableBody, Row, Cell, CheckboxGroup, Checkbox, ActionButton} from '@react-spectrum/s2';
import {useState} from 'react';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
function FileTable() {
let [showColumns, setShowColumns] = useState(['name', 'type', 'date']);
let visibleColumns = columns.filter(column => showColumns.includes(column.id));
let [rows, setRows] = useState(initialRows);
let addRow = () => {
let date = new Date().toLocaleDateString();
setRows(rows => [
...rows,
{id: rows.length + 1, name: 'file.txt', date, type: 'Text Document'}
]);
};
return (
<div className={style({display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'start', width: 'full'})}>
<CheckboxGroup aria-label="Show columns" value={showColumns} onChange={setShowColumns} orientation="horizontal">
<Checkbox value="type">Type</Checkbox>
<Checkbox value="date">Date Modified</Checkbox>
</CheckboxGroup>
<TableView aria-label="Files" styles={style({width: 'full'})}>
<TableHeader columns={visibleColumns}>
{column => (
<Column isRowHeader={column.isRowHeader}>
{column.name}
</Column>
)}
</TableHeader>
<TableBody items={rows} dependencies={[visibleColumns]}>
{item => (
<Row columns={visibleColumns}>
{column => <Cell>{item[column.id]}</Cell>}
</Row>
)}
</TableBody>
</TableView>
<ActionButton onPress={addRow}>Add row</ActionButton>
</div>
);
}
Asynchronous loading
Use the loadingState
and onLoadMore
props to enable async loading and infinite scrolling.
import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
import {useAsyncList} from 'react-stately';
interface Character {
name: string;
height: number;
mass: number;
birth_year: number;
}
function AsyncSortTable() {
let list = useAsyncList<Character>({
async load({ signal, cursor }) {
if (cursor) {
cursor = cursor.replace(/^http:\/\//i, 'https://');
}
let res = await fetch(
cursor || 'https://swapi.py4e.com/api/people/?search=',
{ signal }
);
let json = await res.json();
return {
items: json.results,
cursor: json.next
};
}
});
return (
<TableView
aria-label="Star Wars characters"
loadingState={list.loadingState}
onLoadMore={list.loadMore}
styles={style({width: 'full', height: 320})}>
<TableHeader>
<Column id="name" isRowHeader>Name</Column>
<Column id="height">Height</Column>
<Column id="mass">Mass</Column>
<Column id="birth_year">Birth Year</Column>
</TableHeader>
<TableBody items={list.items}>
{(item) => (
<Row id={item.name}>
<Cell>{item.name}</Cell>
<Cell>{item.height}</Cell>
<Cell>{item.mass}</Cell>
<Cell>{item.birth_year}</Cell>
</Row>
)}
</TableBody>
</TableView>
);
}
Links
Use the href
prop on a Row to create a link. See the client side routing guide to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the selection guide for more details.
Empty state
Use renderEmptyState
to render placeholder content when the table is empty.
import {TableView, TableHeader, Column, TableBody, IllustratedMessage, Heading, Content, Link} from '@react-spectrum/s2';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
import FolderOpen from '@react-spectrum/s2/illustrations/linear/FolderOpen';
<TableView aria-label="Search results" styles={style({width: 'full', height: 320})}>
<TableHeader>
<Column isRowHeader>Name</Column>
<Column>Type</Column>
<Column>Date Modified</Column>
</TableHeader>
renderEmptyState={() => (
<IllustratedMessage>
<FolderOpen />
<Heading>No results</Heading>
<Content>Press <Link href="https://adobe.com">here</Link> for more info.</Content>
</IllustratedMessage>
)}>
{[]}
</TableBody>
</TableView>
Cell options
Use the align
prop on a Column and Cell to set the text alignment. showDivider
adds a divider between a cell and the next cell. colSpan
makes a cell span multiple columns.
import {TableView, TableHeader, Column, TableBody, Row, Cell, Collection} from '@react-spectrum/s2';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
const columns = [
{id: 'name', name: 'Name', isRowHeader: true, showDivider: true},
{id: 'type', name: 'Type', align: 'center', showDivider: true},
{id: 'level', name: 'Level', align: 'end'}
];
function TableWithDividers() {
return (
<TableView
aria-label="Favorite pokemon"
styles={style({width: 400})}>
<TableHeader columns={columns}>
{(column) => (
<Column
showDivider={column.showDivider}
align={column.align}
isRowHeader={column.isRowHeader}>
{column.name}
</Column>
)}
</TableHeader>
<TableBody>
<Collection items={rows}>
{item => (
<Row id={item.id} columns={columns}>
{(column) => (
<Cell
showDivider={column.showDivider}
align={column.align}>
{item[column.id]}
</Cell>
)}
</Row>
)}
</Collection>
<Row>
<Cell colSpan={2} align="end" showDivider>Total:</Cell>
<Cell align="end">{rows.reduce((p, v) => p + v.level, 0)}</Cell>
</Row>
</TableBody>
</TableView>
);
}
Column menus
Use the menuItems
prop to add custom menu items to a Column. See the Menu docs for more details.
import {TableView, TableHeader, Column, TableBody, Row, Cell, MenuSection, MenuItem} from '@react-spectrum/s2';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
function CustomMenusTable() {
return (
<TableView aria-label="Favorite pokemon" styles={style({width: 400})}>
<TableHeader columns={columns}>
{(column) => (
<Column
menuItems={
<>
<MenuSection>
<MenuItem onAction={() => alert(`Filtering "${column.name}" column`)}>Filter</MenuItem>
</MenuSection>
<MenuSection>
<MenuItem onAction={() => alert(`Hiding "${column.name}" column`)}>Hide column</MenuItem>
<MenuItem onAction={() => alert(`Managing the "${column.name}" column`)}>Manage columns</MenuItem>
</MenuSection>
</>
}
isRowHeader={column.isRowHeader}>
{column.name}
</Column>
)}
</TableHeader>
<TableBody items={rows}>
{item => (
<Row id={item.id} columns={columns}>
{(column) => {
return <Cell>{item[column.id]}</Cell>;
}}
</Row>
)}
</TableBody>
</TableView>
);
}
Selection and actions
Use selectionMode
to enable single or multiple selection, and selectedKeys
(matching each row's id
) to control the selected rows. Return an ActionBar from renderActionBar
to handle bulk actions, and use onAction
for row navigation. Disable rows with isDisabled
. See the selection guide for details.
Current selection:
Sorting
Set the allowsSorting
prop on a Column to make it sortable. When the column header is pressed, onSortChange
is called with a SortDescriptor including the sorted column and direction (ascending or descending). Use this to sort the data accordingly, and pass the sortDescriptor
prop to the TableView to display the sorted column.
import {TableView, TableHeader, Column, TableBody, Row, Cell, type SortDescriptor} from '@react-spectrum/s2';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
import {useState} from 'react';
function SortableTable() {
let [sortDescriptor, setSortDescriptor] = useState<SortDescriptor | null>(null);
let sortedRows = rows;
if (sortDescriptor) {
sortedRows = rows.toSorted((a, b) => {
let first = a[sortDescriptor.column];
let second = b[sortDescriptor.column];
let cmp = first < second ? -1 : 1;
if (sortDescriptor.direction === 'descending') {
cmp = -cmp;
}
return cmp;
});
}
return (
<TableView
aria-label="Favorite pokemon"
styles={style({width: 400})}
sortDescriptor={sortDescriptor}
onSortChange={setSortDescriptor}
>
<TableHeader>
<Column id="name" isRowHeader allowsSorting>Name</Column>
<Column id="type" allowsSorting>Type</Column>
<Column id="level" allowsSorting>Level</Column>
</TableHeader>
<TableBody items={sortedRows}>
{item => (
<Row>
<Cell>{item.name}</Cell>
<Cell>{item.type}</Cell>
<Cell>{item.level}</Cell>
</Row>
)}
</TableBody>
</TableView>
);
}
Column resizing
Set the allowsResizing
prop on a Column to make it resizable. Use the defaultWidth
, width
, minWidth
, and maxWidth
props on a Column to control resizing behavior. These accept pixels, percentages, or fractional values (the fr unit). The default column width is 1fr
.
import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
<TableView aria-label="Table with resizable columns" styles={style({width: 400})}>
<TableHeader>
<Column id="file" isRowHeader maxWidth={500} allowsResizing>
File Name
</Column>
<Column id="size" width={80}>Size</Column>
<Column id="date" minWidth={100} allowsResizing>
Date Modified
</Column>
</TableHeader>
<TableBody items={rows}>
{item => (
<Row>
<Cell>{item.name}</Cell>
<Cell>{item.size}</Cell>
<Cell>{item.date}</Cell>
</Row>
)}
</TableBody>
</TableView>
Resize events
The TableView's onResize
event is called when a column resizer is moved by the user. The onResizeEnd
event is called when the user finishes resizing. These receive a Map
containing the widths of all columns in the TableView. This example persists the column widths in localStorage
.
import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
import {useSyncExternalStore} from 'react';
const initialWidths = new Map([
['file', '1fr'],
['size', 80],
['date', 100]
]);
export default function ResizableTable() {
let columnWidths = useSyncExternalStore(subscribe, getColumnWidths, getInitialWidths);
return (
<TableView
aria-label="Table with resizable columns"
onResize={setColumnWidths}
styles={style({width: 400})}>
<TableHeader columns={columns} dependencies={[columnWidths]}>
{column => (
<Column
isRowHeader={column.id === 'file'}
allowsResizing
width={columnWidths.get(column.id)}
>
{column.name}
</Column>
)}
</TableHeader>
<TableBody items={rows}>
{item => (
<Row>
<Cell>{item.name}</Cell>
<Cell>{item.size}</Cell>
<Cell>{item.date}</Cell>
</Row>
)}
</TableBody>
</TableView>
);
}
let parsedWidths;
function getColumnWidths() {
// Parse column widths from localStorage.
if (!parsedWidths) {
let data = localStorage.getItem('table-widths');
if (data) {
parsedWidths = new Map(JSON.parse(data));
}
}
return parsedWidths || initialWidths;
}
function setColumnWidths(widths) {
// Store new widths in localStorage, and trigger subscriptions.
localStorage.setItem('table-widths', JSON.stringify(Array.from(widths)));
window.dispatchEvent(new Event('storage'));
}
function getInitialWidths() {
return initialWidths;
}
function subscribe(fn) {
let onStorage = () => {
// Invalidate cache.
parsedWidths = null;
fn();
};
window.addEventListener('storage', onStorage);
return () => window.removeEventListener('storage', onStorage);
}
API
<TableView>
<TableHeader>
<Column />
</TableHeader>
<TableBody>
<Row>
<Cell />
</Row>
</TableBody>
</TableView>
TableView
Name | Type | Default |
---|---|---|
styles | StylesPropWithHeight | Default: — |
Spectrum-defined styles, returned by the style() macro. | ||
children | ReactNode | Default: — |
The elements that make up the table. Includes the TableHeader, TableBody, Columns, and Rows. | ||
sortDescriptor | SortDescriptor | Default: — |
The current sorted column and direction. | ||
isQuiet | boolean | Default: — |
Whether the Table should be displayed with a quiet style. | ||
density | 'compact'
| 'spacious'
| 'regular' | Default: 'regular'
|
Sets the amount of vertical padding within each cell. | ||
overflowMode | 'wrap' | 'truncate' | Default: 'truncate'
|
Sets the overflow behavior for the cell contents. | ||
renderActionBar |
| Default: — |
Provides the ActionBar to display when rows are selected in the TableView. | ||
loadingState | LoadingState | Default: — |
The current loading state of the table. | ||
onLoadMore |
| Default: — |
Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. | ||
selectionMode | SelectionMode | Default: — |
The type of selection that is allowed in the collection. | ||
selectedKeys | 'all' | Iterable | Default: — |
The currently selected keys in the collection (controlled). | ||
defaultSelectedKeys | 'all' | Iterable | Default: — |
The initial selected keys in the collection (uncontrolled). | ||
onSelectionChange |
| Default: — |
Handler that is called when the selection changes. | ||
disabledKeys | Iterable | Default: — |
A list of row keys to disable. | ||
disallowEmptySelection | boolean | Default: — |
Whether the collection allows empty selection. | ||
shouldSelectOnPressUp | boolean | Default: — |
Whether selection should occur on press up instead of press down. | ||
escapeKeyBehavior | 'clearSelection' | 'none' | Default: 'clearSelection'
|
Whether pressing the escape key should clear selection in the table or not. Most experiences should not modify this option as it eliminates a keyboard user's ability to easily clear selection. Only use if the escape key is being handled externally or should not trigger selection clearing contextually. | ||
TableHeader
A header within a <Table>
, containing the table columns.
Name | Type | |
---|---|---|
children | ReactNode | | |
A list of Column(s) or a function. If the latter, a list of columns must be provided using the columns prop. | ||
columns | Iterable | |
A list of table columns. | ||
dependencies | ReadonlyArray | |
Values that should invalidate the column cache when using dynamic collections. |
Column
A column within a <Table>
.
Name | Type | |
---|---|---|
showDivider | boolean | |
Whether the column should render a divider between it and the next column. | ||
allowsResizing | boolean | |
Whether the column allows resizing. | ||
children | ReactNode | |
The content to render as the column header. | ||
menuItems | ReactNode | |
Menu fragment to be rendered inside the column header's menu. | ||
id | Key | |
The unique id of the column. | ||
allowsSorting | boolean | |
Whether the column allows sorting. | ||
isRowHeader | boolean | |
Whether a column is a row header and should be announced by assistive technology during row navigation. | ||
textValue | string | |
A string representation of the column's contents, used for accessibility announcements. | ||
width | ColumnSize | null | |
The width of the column. This prop only applies when the <Table> is wrapped in a <ResizableTableContainer> . | ||
defaultWidth | ColumnSize | null | |
The default width of the column. This prop only applies when the <Table> is wrapped in a <ResizableTableContainer> . | ||
minWidth | ColumnStaticSize | null | |
The minimum width of the column. This prop only applies when the <Table> is wrapped in a <ResizableTableContainer> . | ||
maxWidth | ColumnStaticSize | null | |
The maximum width of the column. This prop only applies when the <Table> is wrapped in a <ResizableTableContainer> . | ||
TableBody
The body of a <Table>
, containing the table rows.
Name | Type | |
---|---|---|
children | ReactNode | | |
The contents of the collection. | ||
items | Iterable | |
Item objects in the collection. | ||
renderEmptyState |
| |
Provides content to display when there are no rows in the table. | ||
dependencies | ReadonlyArray | |
Values that should invalidate the item cache when using dynamic collections. |
Row
A row within a <Table>
.
Name | Type | |
---|---|---|
textValue | string | |
A string representation of the row's contents, used for features like typeahead. | ||
id | Key | |
The unique id of the row. | ||
children | ReactNode | | |
The cells within the row. Supports static items or a function for dynamic rendering. | ||
columns | Iterable | |
A list of columns used when dynamically rendering cells. | ||
dependencies | ReadonlyArray | |
Values that should invalidate the cell cache when using dynamic collections. |
Cell
A cell within a table row.
Name | Type | |
---|---|---|
children | ReactNode | |
The content to render as the cell children. | ||
id | Key | |
The unique id of the cell. | ||
textValue | string | |
A string representation of the cell's contents, used for features like typeahead. | ||
colSpan | number | |
Indicates how many columns the data cell spans. | ||
showDivider | boolean | |
Whether the column should render a divider between it and the next column. | ||