useTable
Provides the behavior and accessibility implementation for a table component. A table displays data in one or more rows and columns and enables a user to navigate its contents via directional navigation keys.
| install | yarn add @react-aria/table |
|---|---|
| version | 3.0.0-alpha.11 |
| usage | import {useTable, useTableCell, useTableColumnHeader, useTableRow, useTableRowGroup, useTableRowHeader, useTableSelectAllCheckbox, useTableSelectionCheckbox} from '@react-aria/table' |
API#
useTable<T>(
(props: TableProps<T>,
, state: TableState<T>
)): GridAriauseTableColumnHeader<T>(
(props: ColumnHeaderProps,
, state: TableState<T>
)): ColumnHeaderAriauseTableRow<T>(
(props: GridRowProps<T>,
, state: TableState<T>
)): GridRowAriauseTableRowGroup(): GridRowGroupAriauseTableRowHeader<T>(
(props: RowHeaderProps,
, state: TableState<T>
)): RowHeaderAriauseTableCell<T, C>(
(props: GridCellProps,
, state: GridState<T, C>
)): GridCellAriauseTableSelectionCheckbox<T>(
(props: SelectionCheckboxProps,
, state: TableState<T>
)): SelectionCheckboxAriauseTableSelectAllCheckbox<T>(
(state: TableState<T>
)): SelectAllCheckboxAriaFeatures#
A table can be built using the <table>, <thead>,
<tbody>, and other table specific HTML elements, but is very limited in functionality especially when it comes to user interactions.
useTable helps achieve accessible and interactive table components that can be styled as needed.
- Exposed to assistive technology as a
gridusing ARIA - Support for single, multiple, or no row selection
- Support for disabled rows
- Labeling support for accessibility
- Support for mouse, touch, and keyboard interactions
- Support for keyboard navigation between columns, rows, cells, and in-cell focusable items
- Column sorting
- Multiple row header support
- Automatic scrolling support during keyboard navigation
- Typeahead to allow focusing rows by typing text
- Virtualized scrolling support for performance with large tables
Anatomy#
A table consists of a container element, with columns and rows of cells containing data inside. The cells within a table may contain focusable elements or plain text content. If the table supports row selection, each row includes a selection checkbox in the first column. Additionally, a "select all" checkbox is displayed as the first column header if the table supports multiple row selection.
useTable, useTableCell, useTableColumnHeader, useTableRow, and useTableRowHeader handle keyboard, mouse, and other interactions to support
row selection, in table navigation, and overall focus behavior. Those hooks, along with useTableRowGroup, also handle exposing the table and its contents
to assistive technology using ARIA. useTableSelectAllCheckbox and useTableSelectionCheckbox handle row selection and associating each checkbox with its respective rows
for assistive technology.
useTable returns props that you should spread onto the table container element:
| Name | Type | Description |
gridProps | HTMLAttributes<HTMLElement> | Props for the grid element. |
useTableColumnHeader returns props for an individual table column header:
| Name | Type | Description |
columnHeaderProps | HTMLAttributes<HTMLElement> | Props for the column header element. |
useTableRow returns props for an individual table row:
| Name | Type | Description |
rowProps | HTMLAttributes<HTMLElement> | Props for the grid row element. |
useTableRowGroup returns props for a element containing one or more rows of column headers or cells. This is often equivalent
to the header, body, and footer of the table:
| Name | Type | Description |
rowGroupProps | HTMLAttributes<HTMLElement> | Props for the row group element. |
useTableRowHeader returns props for the table cell that serves as a row's header:
| Name | Type | Description |
rowHeaderProps | HTMLAttributes<HTMLElement> | Props for the row header element. |
useTableCell returns props for an individual table cell:
| Name | Type | Description |
gridCellProps | HTMLAttributes<HTMLElement> | Props for the grid cell element. |
useTableSelectionCheckbox returns props for an individual table row selection checkbox:
| Name | Type | Description |
checkboxProps | AriaCheckboxProps | Props for the row selection checkbox element. |
useTableSelectAllCheckbox returns props for the table's "select all" checkbox:
| Name | Type | Description |
checkboxProps | AriaCheckboxProps | Props for the select all checkbox element. |
State is managed by the useTableState
hook from @react-stately/table. The state object should be passed as an option to each of the above hooks where applicable.
Note that an aria-label or aria-labelledby must be passed to the table to identify the element to assistive technology.
State management#
useTable requires knowledge of the rows, cells, and columns in the table in order to handle keyboard
navigation and other interactions. It does this using
the Collection
interface, which is a generic interface to access sequential unique keyed data. You can
implement this interface yourself, e.g. by using a prop to pass a list of item objects,
but useTableState from
@react-stately/table implements a JSX based interface for building collections instead.
See Collection Components for more information,
and Collection Interface for internal details.
In addition, useTableState
manages the state necessary for multiple selection and exposes
a SelectionManager,
which makes use of the collection to provide an interface to update the selection state.
For more information, see Selection.
Example#
This example uses HTML <div> elements to construct the table. It uses useTableState to construct the table's collection of rows and columns,
applying the appropriate attributes to each <div> using the various table hooks listed above paired with the div's role in the table.
The table follows the Collection Components API, accepting both static and dynamic collections.
The example below shows a static collection, which can be used when the full set of table contents is known ahead of time. Be sure to note the usage of
Cell, Column, Row, TableBody, and TableHeader to construct the table structure. These elements are similar to Item and Section used in other components
following the Collection Components API mentioned previously, serving as node generators for the table collection.
See useTableState for more info on the props they support.
import {
Cell
Column
Row
TableBody
TableHeader
useTableState
} from '@react-stately/table';
import {mergeProps} from '@react-aria/utils';
import {useCheckbox} from '@react-aria/checkbox';
import {useContext useRef} from 'react';
import {useFocusRing} from '@react-aria/focus';
import {useToggleState} from '@react-stately/toggle';
const TableContext = ReactcreateContext(null);
function useTableContext() {
return useContext(TableContext);
}
function Table(props) {
let state = useTableState({...props showSelectionCheckboxes: true});
let ref = useRef();
let scrollViewRef = useRef();
let {collection} = state;
let {gridProps} = useTable(
{
...props
ref: ref
}
state
);
let headerRows = collectionrowsfilter(
(item) => itemtype === 'headerrow' && item
);
let rows = collectionrowsfilter((item) => itemtype === 'item' && item);
let renderColumnNode = (node) => {
if (nodepropsisSelectionCell) {
return <TableSelectAllCell column=node />;
}
return <TableColumnHeader column=node />;
};
let renderRowNode = (node) => {
if (nodepropsisSelectionCell) {
return <TableCheckboxCell cell=node />;
}
if (collectionrowHeaderColumnKeyshas(nodecolumnkey)) {
return <TableRowHeader cell=node />;
}
return <TableCell cell=node />;
};
return (
<TableContextProvider value=state>
<div
...gridProps
style={...propsstyle display: 'flex' flexDirection: 'column'}
ref=ref>
<TableHeaderGroup>
[...headerRows]map((headerRow) => (
<TableHeaderRow item=headerRow>
[...headerRowchildNodes]map((column) =>
renderColumnNode(column)
)
</TableHeaderRow>
))
</TableHeaderGroup>
<div
style={
borderTop: '2px solid black'
borderBottom: '2px solid black'
padding: '0px 1px 0px 1px'
flex: '1 1 0%'
overflow: 'auto'
}>
<TableRowGroup>
[...rows]map((row) => (
<TableRow item=row>
[...rowchildNodes]map((node) => renderRowNode(node))
</TableRow>
))
</TableRowGroup>
</div>
</div>
</TableContextProvider>
);
}
function TableHeaderGroup({children ...otherProps}) {
let {rowGroupProps} = useTableRowGroup();
return (
<div ...rowGroupProps ...otherProps>
children
</div>
);
}
function TableColumnHeader({column}) {
let ref = useRef();
let state = useTableContext();
let {columnHeaderProps} = useTableColumnHeader(
{
node: column
ref
colspan: columncolspan
}
state
);
let {isFocusVisible focusProps} = useFocusRing();
let columnProps = columnprops;
let arrowIcon = statesortDescriptor?direction === 'ascending' ? '▲' : '▼';
return (
<div
...mergeProps(columnHeaderProps focusProps)
style={
height: '16px'
lineHeight: '16px'
width: '100px'
padding: '10px 10px 10px 10px'
outline: isFocusVisible ? '2px solid -webkit-focus-ring-color' : 'none'
}
ref=ref>
columnrendered
columnPropsallowsSorting &&
statesortDescriptor?column === columnkey && (
<span aria-hidden="true" style={padding: '0 2px'}>
arrowIcon
</span>
)
</div>
);
}
function TableSelectAllCell({column}) {
let ref = useRef();
let state = useTableContext();
let isSingleSelectionMode = stateselectionManagerselectionMode === 'single';
let {columnHeaderProps} = useTableColumnHeader(
{
node: column
ref
colspan: columncolspan
isDisabled: isSingleSelectionMode
}
state
);
let {checkboxProps} = useTableSelectAllCheckbox(state);
let inputRef = useRef(null);
let {inputProps} = useCheckbox(
checkboxProps
useToggleState(checkboxProps)
inputRef
);
return (
<div
...columnHeaderProps
style={
height: '16px'
padding: '10px 10px 10px 10px'
}
aria-disabled=isSingleSelectionMode
ref=ref>
<input
...inputProps
ref=inputRef
disabled=isSingleSelectionMode
style={visibility: isSingleSelectionMode ? 'hidden' : 'visible'}
/>
</div>
);
}
function TableRowGroup({children ...otherProps}) {
let {rowGroupProps} = useTableRowGroup();
return (
<div ...rowGroupProps ...otherProps>
children
</div>
);
}
function TableRow({item children ...otherProps}) {
let ref = useRef();
let state = useTableContext();
let isDisabled = statedisabledKeyshas(itemkey);
let isSelected = stateselectionManagerisSelected(itemkey) && !isDisabled;
let {rowProps} = useTableRow(
{
node: item
isSelected
ref
isDisabled
}
state
);
let props = mergeProps(rowProps otherProps);
return (
<div
style={
display: 'flex'
flexDirection: 'row'
padding: '1px 1px 2px 1px'
borderTop: '1px solid lightgrey'
}
...props
ref=ref>
children
</div>
);
}
function TableHeaderRow({item children ...otherProps}) {
return (
<div
role="row"
aria-rowindex=itemindex + 1
...otherProps
style={display: 'flex' flexDirection: 'row' paddingBottom: '4px'}>
children
</div>
);
}
function TableCheckboxCell({cell}) {
let ref = useRef();
let state = useTableContext();
let isDisabled = statedisabledKeyshas(cellparentKey);
let {gridCellProps} = useTableCell(
{
node: cell
ref
isDisabled
}
state
);
let {checkboxProps} = useTableSelectionCheckbox(
{
key: cellparentKey
isDisabled
}
state
);
let inputRef = useRef(null);
let {inputProps} = useCheckbox(
{...checkboxProps isDisabled}
useToggleState(checkboxProps)
inputRef
);
return (
<div
...gridCellProps
style={
height: '16px'
padding: '10px 10px 10px 10px'
}
ref=ref>
stateselectionManagerselectionMode !== 'none' && (
<input ...inputProps />
)
</div>
);
}
function TableCell({cell}) {
let ref = useRef();
let state = useTableContext();
let isDisabled = statedisabledKeyshas(cellparentKey);
let {gridCellProps} = useTableCell(
{
node: cell
ref
isDisabled
}
state
);
return <TableCellBase ...gridCellProps cell=cell cellRef=ref />;
}
function TableRowHeader({cell}) {
let ref = useRef();
let state = useTableContext();
let isDisabled = statedisabledKeyshas(cellparentKey);
let {rowHeaderProps} = useTableRowHeader(
{
node: cell
ref
isDisabled
}
state
);
return <TableCellBase ...rowHeaderProps cell=cell cellRef=ref />;
}
function TableCellBase({cell cellRef ...otherProps}) {
let {isFocusVisible focusProps} = useFocusRing();
return (
<div
...mergeProps(otherProps focusProps)
style={
height: '16px'
lineHeight: '16px'
width: '100px'
padding: '10px 10px 10px 10px'
outline: isFocusVisible ? '2px solid -webkit-focus-ring-color' : 'none'
}
ref=cellRef>
<span
style={
overflow: 'hidden'
whiteSpace: 'nowrap'
textOverflow: 'ellipsis'
}>
cellrendered
</span>
</div>
);
}
<Table
aria-label="Example static collection table"
style={height: '210px' maxWidth: '400px'}>
<TableHeader>
<Column>Name</Column>
<Column>Type</Column>
<Column>Date Modified</Column>
</TableHeader>
<TableBody>
<Row>
<Cell>Games</Cell>
<Cell>File folder</Cell>
<Cell>6/7/2020</Cell>
</Row>
<Row>
<Cell>Program Files</Cell>
<Cell>File folder</Cell>
<Cell>4/7/2021</Cell>
</Row>
<Row>
<Cell>bootmgr</Cell>
<Cell>System file</Cell>
<Cell>11/20/2010</Cell>
</Row>
<Row>
<Cell>log.txt</Cell>
<Cell>Text Document</Cell>
<Cell>1/18/2016</Cell>
</Row>
</TableBody>
</Table>import {
Cell
Column
Row
TableBody
TableHeader
useTableState
} from '@react-stately/table';
import {mergeProps} from '@react-aria/utils';
import {useCheckbox} from '@react-aria/checkbox';
import {useContext useRef} from 'react';
import {useFocusRing} from '@react-aria/focus';
import {useToggleState} from '@react-stately/toggle';
const TableContext = ReactcreateContext(null);
function useTableContext() {
return useContext(TableContext);
}
function Table(props) {
let state = useTableState({
...props
showSelectionCheckboxes: true
});
let ref = useRef();
let scrollViewRef = useRef();
let {collection} = state;
let {gridProps} = useTable(
{
...props
ref: ref
}
state
);
let headerRows = collectionrowsfilter(
(item) => itemtype === 'headerrow' && item
);
let rows = collectionrowsfilter(
(item) => itemtype === 'item' && item
);
let renderColumnNode = (node) => {
if (nodepropsisSelectionCell) {
return <TableSelectAllCell column=node />;
}
return <TableColumnHeader column=node />;
};
let renderRowNode = (node) => {
if (nodepropsisSelectionCell) {
return <TableCheckboxCell cell=node />;
}
if (
collectionrowHeaderColumnKeyshas(nodecolumnkey)
) {
return <TableRowHeader cell=node />;
}
return <TableCell cell=node />;
};
return (
<TableContextProvider value=state>
<div
...gridProps
style={
...propsstyle
display: 'flex'
flexDirection: 'column'
}
ref=ref>
<TableHeaderGroup>
[...headerRows]map((headerRow) => (
<TableHeaderRow item=headerRow>
[...headerRowchildNodes]map((column) =>
renderColumnNode(column)
)
</TableHeaderRow>
))
</TableHeaderGroup>
<div
style={
borderTop: '2px solid black'
borderBottom: '2px solid black'
padding: '0px 1px 0px 1px'
flex: '1 1 0%'
overflow: 'auto'
}>
<TableRowGroup>
[...rows]map((row) => (
<TableRow item=row>
[...rowchildNodes]map((node) =>
renderRowNode(node)
)
</TableRow>
))
</TableRowGroup>
</div>
</div>
</TableContextProvider>
);
}
function TableHeaderGroup({children ...otherProps}) {
let {rowGroupProps} = useTableRowGroup();
return (
<div ...rowGroupProps ...otherProps>
children
</div>
);
}
function TableColumnHeader({column}) {
let ref = useRef();
let state = useTableContext();
let {columnHeaderProps} = useTableColumnHeader(
{
node: column
ref
colspan: columncolspan
}
state
);
let {isFocusVisible focusProps} = useFocusRing();
let columnProps = columnprops;
let arrowIcon =
statesortDescriptor?direction === 'ascending'
? '▲'
: '▼';
return (
<div
...mergeProps(columnHeaderProps focusProps)
style={
height: '16px'
lineHeight: '16px'
width: '100px'
padding: '10px 10px 10px 10px'
outline: isFocusVisible
? '2px solid -webkit-focus-ring-color'
: 'none'
}
ref=ref>
columnrendered
columnPropsallowsSorting &&
statesortDescriptor?column === columnkey && (
<span
aria-hidden="true"
style={padding: '0 2px'}>
arrowIcon
</span>
)
</div>
);
}
function TableSelectAllCell({column}) {
let ref = useRef();
let state = useTableContext();
let isSingleSelectionMode =
stateselectionManagerselectionMode === 'single';
let {columnHeaderProps} = useTableColumnHeader(
{
node: column
ref
colspan: columncolspan
isDisabled: isSingleSelectionMode
}
state
);
let {checkboxProps} = useTableSelectAllCheckbox(state);
let inputRef = useRef(null);
let {inputProps} = useCheckbox(
checkboxProps
useToggleState(checkboxProps)
inputRef
);
return (
<div
...columnHeaderProps
style={
height: '16px'
padding: '10px 10px 10px 10px'
}
aria-disabled=isSingleSelectionMode
ref=ref>
<input
...inputProps
ref=inputRef
disabled=isSingleSelectionMode
style={
visibility: isSingleSelectionMode
? 'hidden'
: 'visible'
}
/>
</div>
);
}
function TableRowGroup({children ...otherProps}) {
let {rowGroupProps} = useTableRowGroup();
return (
<div ...rowGroupProps ...otherProps>
children
</div>
);
}
function TableRow({item children ...otherProps}) {
let ref = useRef();
let state = useTableContext();
let isDisabled = statedisabledKeyshas(itemkey);
let isSelected =
stateselectionManagerisSelected(itemkey) &&
!isDisabled;
let {rowProps} = useTableRow(
{
node: item
isSelected
ref
isDisabled
}
state
);
let props = mergeProps(rowProps otherProps);
return (
<div
style={
display: 'flex'
flexDirection: 'row'
padding: '1px 1px 2px 1px'
borderTop: '1px solid lightgrey'
}
...props
ref=ref>
children
</div>
);
}
function TableHeaderRow({item children ...otherProps}) {
return (
<div
role="row"
aria-rowindex=itemindex + 1
...otherProps
style={
display: 'flex'
flexDirection: 'row'
paddingBottom: '4px'
}>
children
</div>
);
}
function TableCheckboxCell({cell}) {
let ref = useRef();
let state = useTableContext();
let isDisabled = statedisabledKeyshas(cellparentKey);
let {gridCellProps} = useTableCell(
{
node: cell
ref
isDisabled
}
state
);
let {checkboxProps} = useTableSelectionCheckbox(
{
key: cellparentKey
isDisabled
}
state
);
let inputRef = useRef(null);
let {inputProps} = useCheckbox(
{...checkboxProps isDisabled}
useToggleState(checkboxProps)
inputRef
);
return (
<div
...gridCellProps
style={
height: '16px'
padding: '10px 10px 10px 10px'
}
ref=ref>
stateselectionManagerselectionMode !== 'none' && (
<input ...inputProps />
)
</div>
);
}
function TableCell({cell}) {
let ref = useRef();
let state = useTableContext();
let isDisabled = statedisabledKeyshas(cellparentKey);
let {gridCellProps} = useTableCell(
{
node: cell
ref
isDisabled
}
state
);
return (
<TableCellBase
...gridCellProps
cell=cell
cellRef=ref
/>
);
}
function TableRowHeader({cell}) {
let ref = useRef();
let state = useTableContext();
let isDisabled = statedisabledKeyshas(cellparentKey);
let {rowHeaderProps} = useTableRowHeader(
{
node: cell
ref
isDisabled
}
state
);
return (
<TableCellBase
...rowHeaderProps
cell=cell
cellRef=ref
/>
);
}
function TableCellBase({cell cellRef ...otherProps}) {
let {isFocusVisible focusProps} = useFocusRing();
return (
<div
...mergeProps(otherProps focusProps)
style={
height: '16px'
lineHeight: '16px'
width: '100px'
padding: '10px 10px 10px 10px'
outline: isFocusVisible
? '2px solid -webkit-focus-ring-color'
: 'none'
}
ref=cellRef>
<span
style={
overflow: 'hidden'
whiteSpace: 'nowrap'
textOverflow: 'ellipsis'
}>
cellrendered
</span>
</div>
);
}
<Table
aria-label="Example static collection table"
style={height: '210px' maxWidth: '400px'}>
<TableHeader>
<Column>Name</Column>
<Column>Type</Column>
<Column>Date Modified</Column>
</TableHeader>
<TableBody>
<Row>
<Cell>Games</Cell>
<Cell>File folder</Cell>
<Cell>6/7/2020</Cell>
</Row>
<Row>
<Cell>Program Files</Cell>
<Cell>File folder</Cell>
<Cell>4/7/2021</Cell>
</Row>
<Row>
<Cell>bootmgr</Cell>
<Cell>System file</Cell>
<Cell>11/20/2010</Cell>
</Row>
<Row>
<Cell>log.txt</Cell>
<Cell>Text Document</Cell>
<Cell>1/18/2016</Cell>
</Row>
</TableBody>
</Table>import {
Cell
Column
Row
TableBody
TableHeader
useTableState
} from '@react-stately/table';
import {mergeProps} from '@react-aria/utils';
import {useCheckbox} from '@react-aria/checkbox';
import {
useContext
useRef
} from 'react';
import {useFocusRing} from '@react-aria/focus';
import {useToggleState} from '@react-stately/toggle';
const TableContext = ReactcreateContext(
null
);
function useTableContext() {
return useContext(
TableContext
);
}
function Table(props) {
let state = useTableState(
{
...props
showSelectionCheckboxes: true
}
);
let ref = useRef();
let scrollViewRef = useRef();
let {
collection
} = state;
let {
gridProps
} = useTable(
{
...props
ref: ref
}
state
);
let headerRows = collectionrowsfilter(
(item) =>
itemtype ===
'headerrow' &&
item
);
let rows = collectionrowsfilter(
(item) =>
itemtype ===
'item' && item
);
let renderColumnNode = (
node
) => {
if (
nodeprops
isSelectionCell
) {
return (
<TableSelectAllCell
column=node
/>
);
}
return (
<TableColumnHeader
column=node
/>
);
};
let renderRowNode = (
node
) => {
if (
nodeprops
isSelectionCell
) {
return (
<TableCheckboxCell
cell=node
/>
);
}
if (
collectionrowHeaderColumnKeyshas(
nodecolumnkey
)
) {
return (
<TableRowHeader
cell=node
/>
);
}
return (
<TableCell
cell=node
/>
);
};
return (
<TableContextProvider
value=state>
<div
...gridProps
style={
...propsstyle
display:
'flex'
flexDirection:
'column'
}
ref=ref>
<TableHeaderGroup>
[
...headerRows
]map(
(
headerRow
) => (
<TableHeaderRow
item=
headerRow
>
[
...headerRowchildNodes
]map(
(
column
) =>
renderColumnNode(
column
)
)
</TableHeaderRow>
)
)
</TableHeaderGroup>
<div
style={
borderTop:
'2px solid black'
borderBottom:
'2px solid black'
padding:
'0px 1px 0px 1px'
flex:
'1 1 0%'
overflow:
'auto'
}>
<TableRowGroup>
[
...rows
]map(
(row) => (
<TableRow
item=
row
>
[
...rowchildNodes
]map(
(
node
) =>
renderRowNode(
node
)
)
</TableRow>
)
)
</TableRowGroup>
</div>
</div>
</TableContextProvider>
);
}
function TableHeaderGroup({
children
...otherProps
}) {
let {
rowGroupProps
} = useTableRowGroup();
return (
<div
...rowGroupProps
...otherProps>
children
</div>
);
}
function TableColumnHeader({
column
}) {
let ref = useRef();
let state = useTableContext();
let {
columnHeaderProps
} = useTableColumnHeader(
{
node: column
ref
colspan:
columncolspan
}
state
);
let {
isFocusVisible
focusProps
} = useFocusRing();
let columnProps =
columnprops;
let arrowIcon =
statesortDescriptor
?direction ===
'ascending'
? '▲'
: '▼';
return (
<div
...mergeProps(
columnHeaderProps
focusProps
)
style={
height: '16px'
lineHeight:
'16px'
width: '100px'
padding:
'10px 10px 10px 10px'
outline: isFocusVisible
? '2px solid -webkit-focus-ring-color'
: 'none'
}
ref=ref>
columnrendered
columnPropsallowsSorting &&
state
sortDescriptor
?column ===
columnkey && (
<span
aria-hidden="true"
style={
padding:
'0 2px'
}>
arrowIcon
</span>
)
</div>
);
}
function TableSelectAllCell({
column
}) {
let ref = useRef();
let state = useTableContext();
let isSingleSelectionMode =
state
selectionManager
selectionMode ===
'single';
let {
columnHeaderProps
} = useTableColumnHeader(
{
node: column
ref
colspan:
columncolspan
isDisabled: isSingleSelectionMode
}
state
);
let {
checkboxProps
} = useTableSelectAllCheckbox(
state
);
let inputRef = useRef(
null
);
let {
inputProps
} = useCheckbox(
checkboxProps
useToggleState(
checkboxProps
)
inputRef
);
return (
<div
...columnHeaderProps
style={
height: '16px'
padding:
'10px 10px 10px 10px'
}
aria-disabled=
isSingleSelectionMode
ref=ref>
<input
...inputProps
ref=inputRef
disabled=
isSingleSelectionMode
style={
visibility: isSingleSelectionMode
? 'hidden'
: 'visible'
}
/>
</div>
);
}
function TableRowGroup({
children
...otherProps
}) {
let {
rowGroupProps
} = useTableRowGroup();
return (
<div
...rowGroupProps
...otherProps>
children
</div>
);
}
function TableRow({
item
children
...otherProps
}) {
let ref = useRef();
let state = useTableContext();
let isDisabled = statedisabledKeyshas(
itemkey
);
let isSelected =
stateselectionManagerisSelected(
itemkey
) && !isDisabled;
let {
rowProps
} = useTableRow(
{
node: item
isSelected
ref
isDisabled
}
state
);
let props = mergeProps(
rowProps
otherProps
);
return (
<div
style={
display: 'flex'
flexDirection:
'row'
padding:
'1px 1px 2px 1px'
borderTop:
'1px solid lightgrey'
}
...props
ref=ref>
children
</div>
);
}
function TableHeaderRow({
item
children
...otherProps
}) {
return (
<div
role="row"
aria-rowindex=
itemindex + 1
...otherProps
style={
display: 'flex'
flexDirection:
'row'
paddingBottom:
'4px'
}>
children
</div>
);
}
function TableCheckboxCell({
cell
}) {
let ref = useRef();
let state = useTableContext();
let isDisabled = statedisabledKeyshas(
cellparentKey
);
let {
gridCellProps
} = useTableCell(
{
node: cell
ref
isDisabled
}
state
);
let {
checkboxProps
} = useTableSelectionCheckbox(
{
key:
cellparentKey
isDisabled
}
state
);
let inputRef = useRef(
null
);
let {
inputProps
} = useCheckbox(
{
...checkboxProps
isDisabled
}
useToggleState(
checkboxProps
)
inputRef
);
return (
<div
...gridCellProps
style={
height: '16px'
padding:
'10px 10px 10px 10px'
}
ref=ref>
state
selectionManager
selectionMode !==
'none' && (
<input
...inputProps
/>
)
</div>
);
}
function TableCell({
cell
}) {
let ref = useRef();
let state = useTableContext();
let isDisabled = statedisabledKeyshas(
cellparentKey
);
let {
gridCellProps
} = useTableCell(
{
node: cell
ref
isDisabled
}
state
);
return (
<TableCellBase
...gridCellProps
cell=cell
cellRef=ref
/>
);
}
function TableRowHeader({
cell
}) {
let ref = useRef();
let state = useTableContext();
let isDisabled = statedisabledKeyshas(
cellparentKey
);
let {
rowHeaderProps
} = useTableRowHeader(
{
node: cell
ref
isDisabled
}
state
);
return (
<TableCellBase
...rowHeaderProps
cell=cell
cellRef=ref
/>
);
}
function TableCellBase({
cell
cellRef
...otherProps
}) {
let {
isFocusVisible
focusProps
} = useFocusRing();
return (
<div
...mergeProps(
otherProps
focusProps
)
style={
height: '16px'
lineHeight:
'16px'
width: '100px'
padding:
'10px 10px 10px 10px'
outline: isFocusVisible
? '2px solid -webkit-focus-ring-color'
: 'none'
}
ref=cellRef>
<span
style={
overflow:
'hidden'
whiteSpace:
'nowrap'
textOverflow:
'ellipsis'
}>
cellrendered
</span>
</div>
);
}
<Table
aria-label="Example static collection table"
style={
height: '210px'
maxWidth: '400px'
}>
<TableHeader>
<Column>
Name
</Column>
<Column>
Type
</Column>
<Column>
Date Modified
</Column>
</TableHeader>
<TableBody>
<Row>
<Cell>
Games
</Cell>
<Cell>
File folder
</Cell>
<Cell>
6/7/2020
</Cell>
</Row>
<Row>
<Cell>
Program Files
</Cell>
<Cell>
File folder
</Cell>
<Cell>
4/7/2021
</Cell>
</Row>
<Row>
<Cell>
bootmgr
</Cell>
<Cell>
System file
</Cell>
<Cell>
11/20/2010
</Cell>
</Row>
<Row>
<Cell>
log.txt
</Cell>
<Cell>
Text Document
</Cell>
<Cell>
1/18/2016
</Cell>
</Row>
</TableBody>
</Table>Usage#
Dynamic collections#
Dynamic collections, as shown below, can be used when the table data comes from an external data source such as an API call, or update over time. In the example below, both the columns and the rows are provided to the table via a render function.
function Example() {
let columns = [
{name: 'Name' key: 'name'}
{name: 'Type' key: 'type'}
{name: 'Date Modified' key: 'date'}
];
let rows = [
{id: '1' name: 'Games' date: '6/7/2020' type: 'File folder'}
{id: '2' name: 'Program Files' date: '4/7/2021' type: 'File folder'}
{id: '3' name: 'bootmgr' date: '11/20/2010' type: 'System file'}
{id: '4' name: 'log.txt' date: '1/18/2016' type: 'Text Document'}
];
return (
<Table
aria-label="Example dynamic collection table"
style={height: '210px' maxWidth: '400px'}>
<TableHeader columns=columns>
(column) => <Column key=columnkey>columnname</Column>
</TableHeader>
<TableBody items=rows>
(item) => (
<Row key=itemid>
/* Note this key is equal to the key of the the column,
not the key set on the Row prior */
(key) => <Cell>item[key]</Cell>
</Row>
)
</TableBody>
</Table>
);
}
function Example() {
let columns = [
{name: 'Name' key: 'name'}
{name: 'Type' key: 'type'}
{name: 'Date Modified' key: 'date'}
];
let rows = [
{
id: '1'
name: 'Games'
date: '6/7/2020'
type: 'File folder'
}
{
id: '2'
name: 'Program Files'
date: '4/7/2021'
type: 'File folder'
}
{
id: '3'
name: 'bootmgr'
date: '11/20/2010'
type: 'System file'
}
{
id: '4'
name: 'log.txt'
date: '1/18/2016'
type: 'Text Document'
}
];
return (
<Table
aria-label="Example dynamic collection table"
style={height: '210px' maxWidth: '400px'}>
<TableHeader columns=columns>
(column) => (
<Column key=columnkey>columnname</Column>
)
</TableHeader>
<TableBody items=rows>
(item) => (
<Row key=itemid>
/* Note this key is equal to the key of the the column,
not the key set on the Row prior */
(key) => <Cell>item[key]</Cell>
</Row>
)
</TableBody>
</Table>
);
}
function Example() {
let columns = [
{
name: 'Name'
key: 'name'
}
{
name: 'Type'
key: 'type'
}
{
name:
'Date Modified'
key: 'date'
}
];
let rows = [
{
id: '1'
name: 'Games'
date: '6/7/2020'
type: 'File folder'
}
{
id: '2'
name:
'Program Files'
date: '4/7/2021'
type: 'File folder'
}
{
id: '3'
name: 'bootmgr'
date: '11/20/2010'
type: 'System file'
}
{
id: '4'
name: 'log.txt'
date: '1/18/2016'
type:
'Text Document'
}
];
return (
<Table
aria-label="Example dynamic collection table"
style={
height: '210px'
maxWidth: '400px'
}>
<TableHeader
columns=
columns
>
(column) => (
<Column
key=
columnkey
>
columnname
</Column>
)
</TableHeader>
<TableBody
items=rows>
(item) => (
<Row
key=
itemid
>
/* Note this key is equal to the key of the the column,
not the key set on the Row prior */
(key) => (
<Cell>
item[
key
]
</Cell>
)
</Row>
)
</TableBody>
</Table>
);
}
Selection#
By default, useTableState doesn't allow row selection but this can be modified using the selectionMode prop. Use defaultSelectedKeys to provide a default set of selected rows.
Note that the value of the selected keys must match the key prop of the row.
The example below uses defaultSelectedKeys to select the row with key equal to "2".
<Table
aria-label="Example table with single selection (uncontrolled)"
selectionMode="single"
defaultSelectedKeys=['2']
style={height: '210px' maxWidth: '400px'}>
<TableHeader>
<Column>Name</Column>
<Column>Type</Column>
<Column>Level</Column>
</TableHeader>
<TableBody>
<Row key="1">
<Cell>Charizard</Cell>
<Cell>Fire, Flying</Cell>
<Cell>67</Cell>
</Row>
<Row key="2">
<Cell>Blastoise</Cell>
<Cell>Water</Cell>
<Cell>56</Cell>
</Row>
<Row key="3">
<Cell>Venusaur</Cell>
<Cell>Grass, Poison</Cell>
<Cell>83</Cell>
</Row>
<Row key="4">
<Cell>Pikachu</Cell>
<Cell>Electric</Cell>
<Cell>100</Cell>
</Row>
</TableBody>
</Table><Table
aria-label="Example table with single selection (uncontrolled)"
selectionMode="single"
defaultSelectedKeys=['2']
style={height: '210px' maxWidth: '400px'}>
<TableHeader>
<Column>Name</Column>
<Column>Type</Column>
<Column>Level</Column>
</TableHeader>
<TableBody>
<Row key="1">
<Cell>Charizard</Cell>
<Cell>Fire, Flying</Cell>
<Cell>67</Cell>
</Row>
<Row key="2">
<Cell>Blastoise</Cell>
<Cell>Water</Cell>
<Cell>56</Cell>
</Row>
<Row key="3">
<Cell>Venusaur</Cell>
<Cell>Grass, Poison</Cell>
<Cell>83</Cell>
</Row>
<Row key="4">
<Cell>Pikachu</Cell>
<Cell>Electric</Cell>
<Cell>100</Cell>
</Row>
</TableBody>
</Table><Table
aria-label="Example table with single selection (uncontrolled)"
selectionMode="single"
defaultSelectedKeys=[
'2'
]
style={
height: '210px'
maxWidth: '400px'
}>
<TableHeader>
<Column>
Name
</Column>
<Column>
Type
</Column>
<Column>
Level
</Column>
</TableHeader>
<TableBody>
<Row key="1">
<Cell>
Charizard
</Cell>
<Cell>
Fire, Flying
</Cell>
<Cell>67</Cell>
</Row>
<Row key="2">
<Cell>
Blastoise
</Cell>
<Cell>
Water
</Cell>
<Cell>56</Cell>
</Row>
<Row key="3">
<Cell>
Venusaur
</Cell>
<Cell>
Grass, Poison
</Cell>
<Cell>83</Cell>
</Row>
<Row key="4">
<Cell>
Pikachu
</Cell>
<Cell>
Electric
</Cell>
<Cell>100</Cell>
</Row>
</TableBody>
</Table>To programmatically control row selection, use the selectedKeys prop paired with the onSelectionChange callback. The key prop from the selected row will
be passed into the callback when the row is pressed, allowing you to update selectedKeys accordingly.
Here is how you would control selection for the above example.
function PokemonTable(props) {
let columns = [
{name: 'Name' uid: 'name'}
{name: 'Type' uid: 'type'}
{name: 'Level' uid: 'level'}
];
let rows = [
{id: '1' name: 'Charizard' type: 'Fire, Flying' level: '67'}
{id: '2' name: 'Blastoise' type: 'Water' level: '56'}
{id: '3' name: 'Venusaur' type: 'Grass, Poison' level: '83'}
{id: '4' name: 'Pikachu' type: 'Electric' level: '100'}
];
let [selected setSelected] = ReactuseState(new Set(['2']));
return (
<Table
aria-label="Example table with single selection (controlled)"
selectionMode="single"
selectedKeys=selected
onSelectionChange=setSelected
style={height: '210px' maxWidth: '400px'}
...props>
<TableHeader columns=columns>
(column) => <Column key=columnuid>columnname</Column>
</TableHeader>
<TableBody items=rows>
(item) => <Row key=itemid>(key) => <Cell>item[key]</Cell></Row>
</TableBody>
</Table>
);
}
function PokemonTable(props) {
let columns = [
{name: 'Name' uid: 'name'}
{name: 'Type' uid: 'type'}
{name: 'Level' uid: 'level'}
];
let rows = [
{
id: '1'
name: 'Charizard'
type: 'Fire, Flying'
level: '67'
}
{
id: '2'
name: 'Blastoise'
type: 'Water'
level: '56'
}
{
id: '3'
name: 'Venusaur'
type: 'Grass, Poison'
level: '83'
}
{
id: '4'
name: 'Pikachu'
type: 'Electric'
level: '100'
}
];
let [selected setSelected] = ReactuseState(
new Set(['2'])
);
return (
<Table
aria-label="Example table with single selection (controlled)"
selectionMode="single"
selectedKeys=selected
onSelectionChange=setSelected
style={height: '210px' maxWidth: '400px'}
...props>
<TableHeader columns=columns>
(column) => (
<Column key=columnuid>columnname</Column>
)
</TableHeader>
<TableBody items=rows>
(item) => (
<Row key=itemid>
(key) => <Cell>item[key]</Cell>
</Row>
)
</TableBody>
</Table>
);
}
function PokemonTable(
props
) {
let columns = [
{
name: 'Name'
uid: 'name'
}
{
name: 'Type'
uid: 'type'
}
{
name: 'Level'
uid: 'level'
}
];
let rows = [
{
id: '1'
name: 'Charizard'
type:
'Fire, Flying'
level: '67'
}
{
id: '2'
name: 'Blastoise'
type: 'Water'
level: '56'
}
{
id: '3'
name: 'Venusaur'
type:
'Grass, Poison'
level: '83'
}
{
id: '4'
name: 'Pikachu'
type: 'Electric'
level: '100'
}
];
let [
selected
setSelected
] = ReactuseState(
new Set(['2'])
);
return (
<Table
aria-label="Example table with single selection (controlled)"
selectionMode="single"
selectedKeys=
selected
onSelectionChange=
setSelected
style={
height: '210px'
maxWidth: '400px'
}
...props>
<TableHeader
columns=
columns
>
(column) => (
<Column
key=
columnuid
>
columnname
</Column>
)
</TableHeader>
<TableBody
items=rows>
(item) => (
<Row
key=
itemid
>
(key) => (
<Cell>
item[
key
]
</Cell>
)
</Row>
)
</TableBody>
</Table>
);
}
Multiple selection can be enabled by setting selectionMode to multiple.
// Using the same table as above
<PokemonTable selectionMode="multiple" />// Using the same table as above
<PokemonTable selectionMode="multiple" />// Using the same table as above
<PokemonTable selectionMode="multiple" />Table also supports a disallowEmptySelection prop which forces the user to have at least one row in the Table selected at all times.
// Using the same table as above
<PokemonTable selectionMode="single" disallowEmptySelection />// Using the same table as above
<PokemonTable
selectionMode="single"
disallowEmptySelection
/>// Using the same table as above
<PokemonTable
selectionMode="single"
disallowEmptySelection
/>Disabled rows#
You can disable specific rows by providing an array of keys to useTableState via the disabledKeys prop. This will prevent rows from being selectable as shown in the example below.
Note that you are responsible for the styling of disabled rows.
// Using the same table as above
<PokemonTable selectionMode="multiple" disabledKeys=['2'] />// Using the same table as above
<PokemonTable
selectionMode="multiple"
disabledKeys=['2']
/>// Using the same table as above
<PokemonTable
selectionMode="multiple"
disabledKeys=['2']
/>Sorting#
Table supports sorting its data when a column header is pressed. To designate that a Column should support sorting, provide it with
the allowsSorting prop. The Table accepts a SortDescriptor prop that defines the current column key to sort by and the sort direction (ascending/descending).
When the user presses a column's sort icon, the column's key and sort direction is passed into the onSortChange callback, allowing you to update
the SortDescriptor appropriately.
This example performs client side sorting by passing a sort function to the useAsyncList hook.
See the docs for more information on how to perform server side sorting.
import {useAsyncList} from '@react-stately/data';
function AsyncSortTable() {
let columns = [
{name: 'Name' key: 'name'}
{name: 'Height' key: 'height'}
{name: 'Mass' key: 'mass'}
{name: 'Birth Year' key: 'birth_year'}
];
let list = useAsyncList({
async load({signal}) {
let res = await fetch(`https://swapi.dev/api/people/?search` {signal});
let json = await resjson();
return {
items: jsonresults
};
}
async sort({items sortDescriptor}) {
let sorted = itemssort((a b) => {
let cmp;
let first = a[sortDescriptorcolumn]replace('BBY' '');
let second = b[sortDescriptorcolumn]replace('BBY' '');
if (+first || +second) {
cmp = +first < +second ? -1 : 1;
} else {
cmp = first <= second ? -1 : 1;
}
if (sortDescriptordirection === 'descending') {
cmp *= -1;
}
return cmp;
});
return {
items: sorted
};
}
});
return (
<Table
aria-label="Example table with client side sorting"
sortDescriptor=listsortDescriptor
onSortChange=listsort
style={height: '300px' maxWidth: '600px'}>
<TableHeader columns=columns>
(column) => <Column allowsSorting>columnname</Column>
</TableHeader>
<TableBody items=listitems>
(item) => (
<Row key=itemname>(key) => <Cell>item[key]</Cell></Row>
)
</TableBody>
</Table>
);
}
import {useAsyncList} from '@react-stately/data';
function AsyncSortTable() {
let columns = [
{name: 'Name' key: 'name'}
{name: 'Height' key: 'height'}
{name: 'Mass' key: 'mass'}
{name: 'Birth Year' key: 'birth_year'}
];
let list = useAsyncList({
async load({signal}) {
let res = await fetch(
`https://swapi.dev/api/people/?search`
{signal}
);
let json = await resjson();
return {
items: jsonresults
};
}
async sort({items sortDescriptor}) {
let sorted = itemssort((a b) => {
let cmp;
let first = a[sortDescriptorcolumn]replace(
'BBY'
''
);
let second = b[sortDescriptorcolumn]replace(
'BBY'
''
);
if (+first || +second) {
cmp = +first < +second ? -1 : 1;
} else {
cmp = first <= second ? -1 : 1;
}
if (sortDescriptordirection === 'descending') {
cmp *= -1;
}
return cmp;
});
return {
items: sorted
};
}
});
return (
<Table
aria-label="Example table with client side sorting"
sortDescriptor=listsortDescriptor
onSortChange=listsort
style={height: '300px' maxWidth: '600px'}>
<TableHeader columns=columns>
(column) => (
<Column allowsSorting>columnname</Column>
)
</TableHeader>
<TableBody items=listitems>
(item) => (
<Row key=itemname>
(key) => <Cell>item[key]</Cell>
</Row>
)
</TableBody>
</Table>
);
}
import {useAsyncList} from '@react-stately/data';
function AsyncSortTable() {
let columns = [
{
name: 'Name'
key: 'name'
}
{
name: 'Height'
key: 'height'
}
{
name: 'Mass'
key: 'mass'
}
{
name: 'Birth Year'
key: 'birth_year'
}
];
let list = useAsyncList(
{
async load({
signal
}) {
let res = await fetch(
`https://swapi.dev/api/people/?search`
{signal}
);
let json = await resjson();
return {
items:
jsonresults
};
}
async sort({
items
sortDescriptor
}) {
let sorted = itemssort(
(a b) => {
let cmp;
let first = a[
sortDescriptor
column
]replace(
'BBY'
''
);
let second = b[
sortDescriptor
column
]replace(
'BBY'
''
);
if (
+first ||
+second
) {
cmp =
+first <
+second
? -1
: 1;
} else {
cmp =
first <=
second
? -1
: 1;
}
if (
sortDescriptordirection ===
'descending'
) {
cmp *= -1;
}
return cmp;
}
);
return {
items: sorted
};
}
}
);
return (
<Table
aria-label="Example table with client side sorting"
sortDescriptor=
listsortDescriptor
onSortChange=
listsort
style={
height: '300px'
maxWidth: '600px'
}>
<TableHeader
columns=
columns
>
(column) => (
<Column
allowsSorting>
columnname
</Column>
)
</TableHeader>
<TableBody
items=
listitems
>
(item) => (
<Row
key=
itemname
>
(key) => (
<Cell>
item[
key
]
</Cell>
)
</Row>
)
</TableBody>
</Table>
);
}
Internationalization#
useTable handles some aspects of internationalization automatically.
For example, type to select is implemented with an
Intl.Collator
for internationalized string matching. You are responsible for localizing all text content within the table.
Make sure that some types of content (e.g. file extensions) are not translated.
RTL#
In right-to-left languages, the table layout should be mirrored. The columns should be ordered from right to left and the individual column text alignment should be inverted. Ensure that your CSS accounts for this.