A table displays data in rows and columns and enables a user to navigate its contents via directional navigation keys, and optionally supports row selection and sorting.
Vanilla CSS theme
--tint CSS variable used by the Vanilla CSS examples.Name | Type | Date Modified | |
|---|---|---|---|
| Games | File folder | 6/7/2020 | |
| Program Files | File folder | 4/7/2021 | |
| bootmgr | System file | 11/20/2010 | |
| log.txt | Text Document | 1/18/2016 |
Content
Table 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.
Name | Type | Date Modified |
|---|---|---|
| Games | File folder | 6/7/2020 |
| Program Files | File folder | 4/7/2021 |
| bootmgr | System file | 11/20/2010 |
| log.txt | Text Document | 1/18/2016 |
import {Table, TableHeader, Column, Row, TableBody, Cell} from './Table';
import {CheckboxGroup} from './CheckboxGroup';
import {Checkbox} from './Checkbox';
import {Button} from './Button';
import {useState} from 'react';
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 style={{display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'start', width: '100%'}}>
<CheckboxGroup aria-label="Show columns" value={showColumns} onChange={setShowColumns} orientation="horizontal">
<Checkbox value="type">Type</Checkbox>
<Checkbox value="date">Date Modified</Checkbox>
</CheckboxGroup>
<Table aria-label="Files" style={{width: '100%'}}>
<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>
</Table>
<Button onPress={addRow}>Add row</Button>
</div>
);
}
Memoization
dependencies prop to invalidate cached elements that dependon external state (e.g. columns in this example).Asynchronous loading
Use renderEmptyState to display a spinner during initial load. To enable infinite scrolling, render a <TableLoadMoreItem> at the end of the list. Use whatever data fetching library you prefer – this example uses useAsyncList from react-stately.
Name | Height | Mass | Birth Year |
|---|---|---|---|
import {Collection, useAsyncList} from 'react-aria-components';
import {Table, TableHeader, Column, Row, TableBody, Cell, TableLoadMoreItem} from './Table';
import {ProgressCircle} from './ProgressCircle';
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 (
<div
style={{
height: 150,
overflow: 'auto',
border: '0.5px solid var(--border-color)',
borderRadius: 'var(--radius)'
}}>
<Table
aria-label="Star Wars characters"
style={{tableLayout: 'fixed', width: '100%', border: 0}}>
<TableHeader
style={{
position: 'sticky',
top: 0,
background: 'var(--overlay-background)',
zIndex: 1
}}>
<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
renderEmptyState={() => (
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
<ProgressCircle isIndeterminate aria-label="Loading..." />
</div>
)}>
<Collection 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>
)}
</Collection>
<TableLoadMoreItem
onLoadMore={list.loadMore}
isLoading={list.loadingState === 'loadingMore'} />
</TableBody>
</Table>
</div>
);
}
Links
Use the href prop on a <Row> to create a link. See the framework setup guide to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the selection guide for more details.
Name | URL | Date added | |
|---|---|---|---|
| Adobe | https://adobe.com/ | January 28, 2023 | |
| https://google.com/ | April 5, 2023 | ||
| New York Times | https://nytimes.com/ | July 12, 2023 |
Empty state
Name | Type | Date Modified |
|---|---|---|
| No results found. | ||
import {Table, TableHeader, Column, TableBody} from './Table';
<Table aria-label="Search results">
<TableHeader>
<Column isRowHeader>Name</Column>
<Column>Type</Column>
<Column>Date Modified</Column>
</TableHeader>
<TableBody renderEmptyState={() => 'No results found.'}>
{[]}
</TableBody>
</Table>
Selection and actions
Use the selectionMode prop to enable single or multiple selection. The selected rows can be controlled via the selectedKeys prop, matching the id prop of the rows. The onAction event handles item actions. Rows can be disabled with the isDisabled prop. See the selection guide for more details.
Name | Type | Level | |
|---|---|---|---|
| Charizard | Fire, Flying | 67 | |
| Blastoise | Water | 56 | |
| Venusaur | Grass, Poison | 83 | |
| Pikachu | Electric | 100 |
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 <Table> to display the sorted column.
Name | Type | Level |
|---|---|---|
| Blastoise | Water | 56 |
| Charizard | Fire, Flying | 67 |
| Pikachu | Electric | 100 |
| Venusaur | Grass, Poison | 83 |
import {type SortDescriptor} from 'react-aria-components';
import {Table, TableHeader, Column, TableBody, Row, Cell} from './Table';
import {useState} from 'react';
function SortableTable() {
let [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
column: 'name',
direction: 'ascending'
});
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 (
<Table
aria-label="Favorite pokemon"
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>
</Table>
);
}
Column resizing
Wrap the <Table> with a <ResizableTableContainer>, and add a <ColumnResizer> to each 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.
File Name | Size | Date Modified |
|---|---|---|
| 2022 Roadmap Proposal Revision 012822 Copy (2) | 214 KB | November 27, 2022 at 4:56PM |
| Budget | 14 MB | January 27, 2021 at 1:56AM |
| Welcome Email Template | 20 KB | July 24, 2022 at 2:48 PM |
| Job Posting_8301 | 139 KB | May 30, 2025 |
import {Table, TableHeader, Column, Row, TableBody, Cell} from './Table';
import {ResizableTableContainer} from 'react-aria-components';
<ResizableTableContainer>
<Table aria-label="Table with resizable columns">
<TableHeader>
<Column id="file" isRowHeader allowsResizing maxWidth={500}>File Name</Column>
<Column id="size" allowsResizing defaultWidth={80}>Size</Column>
<Column id="date" minWidth={100}>Date Modified</Column>
</TableHeader>
<TableBody items={rows}>
{item => (
<Row>
<Cell>{item.name}</Cell>
<Cell>{item.size}</Cell>
<Cell>{item.date}</Cell>
</Row>
)}
</TableBody>
</Table>
</ResizableTableContainer>
Resize events
The ResizableTableContainer'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 Table. This example persists the column widths in localStorage.
File Name | Size | Date |
|---|---|---|
| 2022 Roadmap Proposal Revision 012822 Copy (2) | 214 KB | November 27, 2022 at 4:56PM |
| Budget | 14 MB | January 27, 2021 at 1:56AM |
| Welcome Email Template | 20 KB | July 24, 2022 at 2:48 PM |
| Job Posting_8301 | 139 KB | May 30, 2025 |
import {Table, TableHeader, Column, Row, TableBody, Cell} from './Table';
import {ResizableTableContainer} from 'react-aria-components';
import {useSyncExternalStore} from 'react';
const initialWidths = new Map<string, number | string>([
['file', '1fr'],
['size', 80],
['date', 100]
]);
export default function ResizableTable() {
let columnWidths = useSyncExternalStore(subscribe, getColumnWidths, getInitialWidths);
return (
<ResizableTableContainer
onResize={setColumnWidths}
>
<Table aria-label="Table with resizable columns">
<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>
</Table>
</ResizableTableContainer>
);
}
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);
}
Drag and drop
Table supports drag and drop interactions when the dragAndDropHooks prop is provided using the useDragAndDrop hook. Users can drop data on the table as a whole, on individual rows, insert new rows between existing ones, or reorder rows. React Aria supports drag and drop via mouse, touch, keyboard, and screen reader interactions. See the drag and drop guide to learn more.
Name | Type | Date Modified | ||
|---|---|---|---|---|
| Games | File folder | 6/7/2020 | ||
| Program Files | File folder | 4/7/2021 | ||
| bootmgr | System file | 11/20/2010 | ||
| log.txt | Text Document | 1/18/2016 |
import {Table, TableHeader, TableBody, Column, Row, Cell} from './Table';
import {useDragAndDrop, useListData} from 'react-aria-components';
function ReorderableTable() {
let list = useListData({
initialItems: [
{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'}
]
});
let {dragAndDropHooks} = useDragAndDrop({
getItems: (keys, items: typeof list.items) => items.map(item => ({
'text/plain': item.name
})),
onReorder(e) {
if (e.target.dropPosition === 'before') {
list.moveBefore(e.target.key, e.keys);
} else if (e.target.dropPosition === 'after') {
list.moveAfter(e.target.key, e.keys);
}
}
});
return (
<Table
aria-label="Files"
selectionMode="multiple"
dragAndDropHooks={dragAndDropHooks}
>
<TableHeader>
<Column isRowHeader>Name</Column>
<Column>Type</Column>
<Column>Date Modified</Column>
</TableHeader>
<TableBody items={list.items}>
{item => (
<Row>
<Cell>{item.name}</Cell>
<Cell>{item.type}</Cell>
<Cell>{item.date}</Cell>
</Row>
)}
</TableBody>
</Table>
);
}
Examples
API
<ResizableTableContainer>
<Table>
<TableHeader>
<Column />
<Column><Checkbox slot="selection" /></Column>
<Column><ColumnResizer /></Column>
<Column />
</TableHeader>
<TableBody>
<Row id="row-1">
<Cell><Button slot="drag" /></Cell>
<Cell>
<Checkbox slot="selection" /> or <SelectionIndicator />
</Cell>
<Cell />
<Cell />
</Row>
<TableLoadMoreItem />
</TableBody>
</Table>
</ResizableTableContainer>
Table
A table displays data in rows and columns and enables a user to navigate its contents via directional navigation keys, and optionally supports row selection and sorting.
| Name | Type | |
|---|---|---|
children | ReactNode | |
| The elements that make up the table. Includes the TableHeader, TableBody, Columns, and Rows. | ||
dragAndDropHooks | DragAndDropHooks | |
The drag and drop hooks returned by useDragAndDrop used to enable drag and drop behavior for the Table. | ||
sortDescriptor | SortDescriptor | |
| The current sorted column and direction. | ||
selectionMode | SelectionMode | |
| The type of selection that is allowed in the collection. | ||
selectionBehavior | SelectionBehavior | |
| How multiple selection should behave in the collection. | ||
selectedKeys | 'all' | Iterable | |
| The currently selected keys in the collection (controlled). | ||
defaultSelectedKeys | 'all' | Iterable | |
| The initial selected keys in the collection (uncontrolled). | ||
onSelectionChange | | |
| Handler that is called when the selection changes. | ||
disabledKeys | Iterable | |
| A list of row keys to disable. | ||
disabledBehavior | DisabledBehavior | |
Whether disabledKeys applies to all interactions, or only selection. | ||
disallowEmptySelection | boolean | |
| Whether the collection allows empty selection. | ||
shouldSelectOnPressUp | boolean | |
| Whether selection should occur on press up instead of press down. | ||
escapeKeyBehavior | 'clearSelection' | 'none' | |
| 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. | ||
Default className: react-aria-Table
| Render Prop | CSS Selector |
|---|---|
isFocused | CSS Selector: [data-focused]
|
| Whether the table is currently focused. | |
isFocusVisible | CSS Selector: [data-focus-visible]
|
| Whether the table is currently keyboard focused. | |
isDropTarget | CSS Selector: [data-drop-target]
|
| Whether the table is currently the active drop target. | |
state | CSS Selector: — |
| State of the table. | |
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. | ||
Default className: react-aria-TableHeader
| Render Prop | CSS Selector |
|---|---|
isHovered | CSS Selector: [data-hovered]
|
| Whether the table header is currently hovered with a mouse. | |
Column
A column within a <Table>.
| Name | Type | |
|---|---|---|
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>. | ||
children | ChildrenOrFunction | |
| The children of the component. A function may be provided to alter the children based on component state. | ||
Default className: react-aria-Column
| Render Prop | CSS Selector |
|---|---|
isHovered | CSS Selector: [data-hovered]
|
| Whether the column is currently hovered with a mouse. | |
isPressed | CSS Selector: [data-pressed]
|
| Whether the column is currently in a pressed state. | |
isFocused | CSS Selector: [data-focused]
|
| Whether the column is currently focused. | |
isFocusVisible | CSS Selector: [data-focus-visible]
|
| Whether the column is currently keyboard focused. | |
allowsSorting | CSS Selector: [data-allows-sorting]
|
| Whether the column allows sorting. | |
sortDirection | CSS Selector: [data-sort-direction="ascending | descending"]
|
| The current sort direction. | |
isResizing | CSS Selector: [data-resizing]
|
| Whether the column is currently being resized. | |
sort | CSS Selector: — |
| Triggers sorting for this column in the given direction. | |
startResize | CSS Selector: — |
Starts column resizing if the table is contained in a <ResizableTableContainer> element. | |
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. | ||
Default className: react-aria-TableBody
| Render Prop | CSS Selector |
|---|---|
isEmpty | CSS Selector: [data-empty]
|
| Whether the table body has no rows and should display its empty state. | |
isDropTarget | CSS Selector: [data-drop-target]
|
| Whether the Table is currently the active drop target. | |
Row
A row within a <Table>.
| Name | Type | |
|---|---|---|
value | object | |
| The object value that this row represents. When using dynamic collections, this is set automatically. | ||
textValue | string | |
| A string representation of the row's contents, used for features like typeahead. | ||
isDisabled | boolean | |
| Whether the row is disabled. | ||
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. | ||
Default className: react-aria-Row
| Render Prop | CSS Selector |
|---|---|
isFocusVisibleWithin | CSS Selector: — |
| Whether the row's children have keyboard focus. | |
isHovered | CSS Selector: [data-hovered]
|
| Whether the item is currently hovered with a mouse. | |
isPressed | CSS Selector: [data-pressed]
|
| Whether the item is currently in a pressed state. | |
isSelected | CSS Selector: [data-selected]
|
| Whether the item is currently selected. | |
isFocused | CSS Selector: [data-focused]
|
| Whether the item is currently focused. | |
isFocusVisible | CSS Selector: [data-focus-visible]
|
| Whether the item is currently keyboard focused. | |
isDisabled | CSS Selector: [data-disabled]
|
Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may
not be focused. Dependent on disabledKeys and disabledBehavior. | |
selectionMode | CSS Selector: [data-selection-mode="single | multiple"]
|
| The type of selection that is allowed in the collection. | |
selectionBehavior | CSS Selector: — |
| The selection behavior for the collection. | |
Cell
A cell within a table row.
| Name | Type | |
|---|---|---|
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. | ||
children | ChildrenOrFunction | |
| The children of the component. A function may be provided to alter the children based on component state. | ||
Default className: react-aria-Cell
| Render Prop | CSS Selector |
|---|---|
isPressed | CSS Selector: [data-pressed]
|
| Whether the cell is currently in a pressed state. | |
isFocused | CSS Selector: [data-focused]
|
| Whether the cell is currently focused. | |
isFocusVisible | CSS Selector: [data-focus-visible]
|
| Whether the cell is currently keyboard focused. | |
isHovered | CSS Selector: [data-hovered]
|
| Whether the cell is currently hovered with a mouse. | |
isSelected | CSS Selector: [data-selected]
|
| Whether the parent row is currently selected. | |
ResizableTableContainer
| Name | Type | |
|---|---|---|
children | ReactNode | |
| The children of the component. | ||
Default className: react-aria-ResizableTableContainer
ColumnResizer
| Name | Type | |
|---|---|---|
children | ChildrenOrFunction | |
| The children of the component. A function may be provided to alter the children based on component state. | ||
Default className: react-aria-ColumnResizer
| Render Prop | CSS Selector |
|---|---|
isHovered | CSS Selector: [data-hovered]
|
| Whether the resizer is currently hovered with a mouse. | |
isFocused | CSS Selector: [data-focused]
|
| Whether the resizer is currently focused. | |
isFocusVisible | CSS Selector: [data-focus-visible]
|
| Whether the resizer is currently keyboard focused. | |
isResizing | CSS Selector: [data-resizing]
|
| Whether the resizer is currently being resized. | |
resizableDirection | CSS Selector: [data-resizable-direction="right | left | both"]
|
| The direction that the column is currently resizable. | |
TableLoadMoreItem
| Name | Type | Default |
|---|---|---|
children | ReactNode | Default: — |
| The load more spinner to render when loading additional items. | ||
isLoading | boolean | Default: — |
| Whether or not the loading spinner should be rendered or not. | ||
scrollOffset | number | Default: 1
|
| The amount of offset from the bottom of your scrollable region that should trigger load more. Uses a percentage value relative to the scroll body's client height. Load more is then triggered when your current scroll position's distance from the bottom of the currently loaded list of items is less than or equal to the provided value. (e.g. 1 = 100% of the scroll region's height). | ||
onLoadMore | | Default: — |
| Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. | ||
Default className: react-aria-TableLoadMoreItem