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 | 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} from './Table';
import {TableBody, Cell} from 'react-aria-components';
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} style={{flexDirection: 'row'}}>
<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>
);
}
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 {Table, TableHeader, Column, Row, TableBody, Cell} from './Table';
import {Collection, TableLoadMoreItem} from 'react-aria-components';
import {ProgressCircle} from './ProgressCircle';
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 (
<div
style={{
height: 150,
overflow: 'auto',
border: '1px solid var(--border-color)',
borderRadius: 6
}}>
<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 allowsSorting>Name</Column>
<Column id="height" allowsSorting>Height</Column>
<Column id="mass" allowsSorting>Mass</Column>
<Column id="birth_year" allowsSorting>Birth Year</Column>
</TableHeader>
<TableBody
renderEmptyState={() => (
<ProgressCircle isIndeterminate aria-label="Loading..." />
)}>
<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'}>
<ProgressCircle isIndeterminate aria-label="Loading more..." />
</TableLoadMoreItem>
</TableBody>
</Table>
</div>
);
}
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.
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, Row} from './Table';
import {TableBody, Cell} from 'react-aria-components';
<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 |
---|---|---|
Charizard | Fire, Flying | 67 |
Blastoise | Water | 56 |
Venusaur | Grass, Poison | 83 |
Pikachu | Electric | 100 |
import {Table, TableHeader, Column, Row} from './Table';
import {TableBody, Cell, type SortDescriptor} from 'react-aria-components';
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 (
<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} from './Table';
import {ResizableTableContainer, ColumnResizer, TableBody, Cell} from 'react-aria-components';
<ResizableTableContainer>
<Table aria-label="Table with resizable columns">
<TableHeader>
<Column id="file" isRowHeader maxWidth={500}>
<div style={{display: 'flex', alignItems: 'center'}}>
<span tabIndex={-1} className="column-name">File Name</span>
<ColumnResizer />
</div>
</Column>
<Column id="size" width={80}>Size</Column>
<Column id="date" minWidth={100}>
<div style={{display: 'flex', alignItems: 'center'}}>
<span tabIndex={-1} className="column-name">Date Modified</span>
<ColumnResizer />
</div>
</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} from './Table';
import {ResizableTableContainer, ColumnResizer, TableBody, Cell} from 'react-aria-components';
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 (
<ResizableTableContainer
onResize={setColumnWidths}
>
<Table aria-label="Table with resizable columns">
<TableHeader columns={columns} dependencies={[columnWidths]}>
{column => (
<Column
isRowHeader={column.id === 'file'}
width={columnWidths.get(column.id)}
>
<div style={{display: 'flex', alignItems: 'center'}}>
<span tabIndex={-1} className="column-name">{column.name}</span>
<ColumnResizer />
</div>
</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 {useListData} from 'react-stately';
import {Table, TableHeader, Column, Row} from './Table';
import {TableBody, Cell} from 'react-aria-components';
import {useDragAndDrop} 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) => [...keys].map(key => ({
'text/plain': list.getItem(key).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>
);
}