Beta Preview

TableView

Tables are containers for displaying information. They allow users to quickly scan, sort, compare, and take action on large amounts of data.

Name
selectionMode 
overflowMode 
density 
isQuiet 
import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};

<TableView
  aria-label="Files"
  selectionMode="multiple"
  styles={style({width: 'full'})}>
  <TableHeader>
    <Column isRowHeader>Name</Column>
    <Column>Type</Column>
    <Column>Date Modified</Column>
  </TableHeader>
  <TableBody>
    <Row id="1">
      <Cell>Projects</Cell>
      <Cell>File folder</Cell>
      <Cell>6/7/2025</Cell>
    </Row>
    <Row id="2">
      <Cell>Pictures</Cell>
      <Cell>File folder</Cell>
      <Cell>4/7/2025</Cell>
    </Row>
    <Row id="3">
      <Cell>2024 Annual Financial Report</Cell>
      <Cell>Text document</Cell>
      <Cell>12/30/2024</Cell>
    </Row>
    <Row id="4">
      <Cell>Job Posting</Cell>
      <Cell>Text Document</Cell>
      <Cell>1/18/2025</Cell>
    </Row>
  </TableBody>
</TableView>

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.

Name
Games
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.

Name
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>
  );
}

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
selectionMode 
import {TableView, TableHeader, Column, TableBody, Row, Cell} from '@react-spectrum/s2';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};

<TableView
  aria-label="Bookmarks"
  selectionMode="multiple"
  styles={style({width: 'full'})}>
  <TableHeader>
    <Column isRowHeader>Name</Column>
    <Column>URL</Column>
    <Column>Date added</Column>
  </TableHeader>
  <TableBody>
    <Row href="https://adobe.com/" target="_blank">
      <Cell>Adobe</Cell>
      <Cell>https://adobe.com/</Cell>
      <Cell>January 28, 2023</Cell>
    </Row>
    <Row href="https://google.com/" target="_blank">
      <Cell>Google</Cell>
      <Cell>https://google.com/</Cell>
      <Cell>April 5, 2023</Cell>
    </Row>
    <Row href="https://nytimes.com/" target="_blank">
      <Cell>New York Times</Cell>
      <Cell>https://nytimes.com/</Cell>
      <Cell>July 12, 2023</Cell>
    </Row>
  </TableBody>
</TableView>

Empty state

Use renderEmptyState to render placeholder content when the table is empty.

Name
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.

Name
Charizard
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.

Charizard
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.

Name

Current selection:

selectionMode 
disallowEmptySelection 
import {TableView, TableHeader, Column, TableBody, Row, Cell, ActionBar, ActionButton, Text, type Selection} from '@react-spectrum/s2';
import Edit from '@react-spectrum/s2/icons/Edit';
import Copy from '@react-spectrum/s2/icons/Copy';
import Delete from '@react-spectrum/s2/icons/Delete';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
import {useState} from 'react';

function Example(props) {
  let [selected, setSelected] = useState<Selection>(new Set());

  return (
    <>
      <TableView
        {...props}
        aria-label="Favorite pokemon"
        styles={style({width: 'full', height: 200})}
        selectionMode="multiple"
        selectedKeys={selected}
        onSelectionChange={setSelected}
        onAction={key => alert(`Clicked ${key}`)}
        renderActionBar={(selectedKeys) => {
          let selection = selectedKeys === 'all' ? 'all' : [...selectedKeys].join(', ');
          return (
            <ActionBar>
              <ActionButton onPress={() => alert(`Edit ${selection}`)}>
                <Edit />
                <Text>Edit</Text>
              </ActionButton>
              <ActionButton onPress={() => alert(`Copy ${selection}`)}>
                <Copy />
                <Text>Copy</Text>
              </ActionButton>
              <ActionButton onPress={() => alert(`Delete ${selection}`)}>
                <Delete />
                <Text>Delete</Text>
              </ActionButton>
            </ActionBar>
          );
        }}>
        <TableHeader>
          <Column isRowHeader>Name</Column>
          <Column>Type</Column>
          <Column>Level</Column>
        </TableHeader>
        <TableBody>
          <Row id="charizard">
            <Cell>Charizard</Cell>
            <Cell>Fire, Flying</Cell>
            <Cell>67</Cell>
          </Row>
          <Row id="blastoise">
            <Cell>Blastoise</Cell>
            <Cell>Water</Cell>
            <Cell>56</Cell>
          </Row>
          <Row id="venusaur" isDisabled>
            <Cell>Venusaur</Cell>
            <Cell>Grass, Poison</Cell>
            <Cell>83</Cell>
          </Row>
          <Row id="pikachu">
            <Cell>Pikachu</Cell>
            <Cell>Electric</Cell>
            <Cell>100</Cell>
          </Row>
        </TableBody>
      </TableView>
      <p>Current selection: {selected === 'all' ? 'all' : [...selected].join(', ')}</p>
    </>
  );
}

Sorting

Set the allowsSorting prop on a Column to make it sortable. When the column header is pressed, onSortChange is called with a 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.

Name
Charizard
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.

2022 Roadmap Proposal Revision 012822 Copy (2)
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.

2022 Roadmap Proposal Revision 012822 Copy (2)
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

NameTypeDefault
stylesDefault:
Spectrum-defined styles, returned by the style() macro.
childrenReactNodeDefault:
The elements that make up the table. Includes the TableHeader, TableBody, Columns, and Rows.
sortDescriptorDefault:
The current sorted column and direction.
isQuietbooleanDefault:
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(selectedKeys: 'all'Set<Key>) => ReactElementDefault:
Provides the ActionBar to display when rows are selected in the TableView.
loadingStateDefault:
The current loading state of the table.
onLoadMore() => anyDefault:
Handler that is called when more items should be loaded, e.g. while scrolling near the bottom.
selectionModeDefault:
The type of selection that is allowed in the collection.
selectedKeys'all'Iterable<Key>Default:
The currently selected keys in the collection (controlled).
defaultSelectedKeys'all'Iterable<Key>Default:
The initial selected keys in the collection (uncontrolled).
onSelectionChange(keys: ) => voidDefault:
Handler that is called when the selection changes.
disabledKeysIterable<Key>Default:
A list of row keys to disable.
disallowEmptySelectionbooleanDefault:
Whether the collection allows empty selection.
shouldSelectOnPressUpbooleanDefault:
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.

NameType
childrenReactNode(item: T) => ReactElement
A list of Column(s) or a function. If the latter, a list of columns must be provided using the columns prop.
columnsIterable<T>
A list of table columns.
dependenciesReadonlyArray<any>
Values that should invalidate the column cache when using dynamic collections.

Column

A column within a <Table>.

NameType
showDividerboolean
Whether the column should render a divider between it and the next column.
allowsResizingboolean
Whether the column allows resizing.
childrenReactNode
The content to render as the column header.
menuItemsReactNode
Menu fragment to be rendered inside the column header's menu.
idKey
The unique id of the column.
allowsSortingboolean
Whether the column allows sorting.
isRowHeaderboolean
Whether a column is a row header and should be announced by assistive technology during row navigation.
textValuestring
A string representation of the column's contents, used for accessibility announcements.
widthnull
The width of the column. This prop only applies when the <Table> is wrapped in a <ResizableTableContainer>.
defaultWidthnull
The default width of the column. This prop only applies when the <Table> is wrapped in a <ResizableTableContainer>.
minWidthnull
The minimum width of the column. This prop only applies when the <Table> is wrapped in a <ResizableTableContainer>.
maxWidthnull
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.

NameType
childrenReactNode(item: T) => ReactNode
The contents of the collection.
itemsIterable<T>
Item objects in the collection.
renderEmptyState(props: ) => ReactNode
Provides content to display when there are no rows in the table.
dependenciesReadonlyArray<any>
Values that should invalidate the item cache when using dynamic collections.

Row

A row within a <Table>.

NameType
textValuestring
A string representation of the row's contents, used for features like typeahead.
idKey
The unique id of the row.
childrenReactNode(item: T) => ReactElement
The cells within the row. Supports static items or a function for dynamic rendering.
columnsIterable<T>
A list of columns used when dynamically rendering cells.
dependenciesReadonlyArray<any>
Values that should invalidate the cell cache when using dynamic collections.

Cell

A cell within a table row.

NameType
childrenReactNode
The content to render as the cell children.
idKey
The unique id of the cell.
textValuestring
A string representation of the cell's contents, used for features like typeahead.
colSpannumber
Indicates how many columns the data cell spans.
showDividerboolean
Whether the column should render a divider between it and the next column.