TableView

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

installyarn add @react-spectrum/table
version3.1.6
usageimport {Cell, Column, Row, TableView, TableBody, TableHeader} from '@react-spectrum/table'

Example#


<TableView
  aria-label="Example table with static contents"
  selectionMode="multiple"
>
  <TableHeader>
    <Column>Name</Column>
    <Column>Type</Column>
    <Column align="end">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>
</TableView>
<TableView
  aria-label="Example table with static contents"
  selectionMode="multiple"
>
  <TableHeader>
    <Column>Name</Column>
    <Column>Type</Column>
    <Column align="end">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>
</TableView>
<TableView
  aria-label="Example table with static contents"
  selectionMode="multiple"
>
  <TableHeader>
    <Column>
      Name
    </Column>
    <Column>
      Type
    </Column>
    <Column align="end">
      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>
</TableView>

Content#


TableView is a complex collection component that is built up from many child elements including columns, rows, and cells. Columns are defined within a TableHeader element and rows are defined within a TableBody element. Rows contain Cell elements that correspond to each column. Cells can contain any element, allowing you to have focusable children within the TableView.

Basic usage of TableView, seen in the example above, shows the use of a static collection where the contents of the TableView is hard coded. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API, or update over time. Providing the data in this way allows TableView to automatically cache the rendering of each item, which dramatically improves performance.

Columns and rows can be statically defined as children, or generated dynamically using a function based on the data passed to the columns or items prop respectively. Cells can also be statically defined as children, or generated dynamically based on the columns defined in the TableHeader.

Each column and row has a unique key defined by the data. In the example below, the uid property of the column object is used as the key for the Column element within the TableHeader. The key of each row element is implicitly defined by the id property of the row object. See collections to learn more keys in dynamic collections.

let columns = [
  {name: 'Name', uid: 'name'},
  {name: 'Type', uid: 'type'},
  {name: 'Date Modified', uid: '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'}
];

<TableView
  aria-label="Example table with dynamic content"
  maxWidth="size-6000">
  <TableHeader columns={columns}>
    {column => (
      <Column
        key={column.uid}
        align={column.uid === 'date' ? 'end' : 'start'}>
        {column.name}
      </Column>
    )}
  </TableHeader>
  <TableBody items={rows}>
    {item => (
      <Row>
        {columnKey => <Cell>{item[columnKey]}</Cell>}
      </Row>
    )}
  </TableBody>
</TableView>
let columns = [
  { name: 'Name', uid: 'name' },
  { name: 'Type', uid: 'type' },
  { name: 'Date Modified', uid: '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'
  }
];

<TableView
  aria-label="Example table with dynamic content"
  maxWidth="size-6000"
>
  <TableHeader columns={columns}>
    {(column) => (
      <Column
        key={column.uid}
        align={column.uid === 'date' ? 'end' : 'start'}
      >
        {column.name}
      </Column>
    )}
  </TableHeader>
  <TableBody items={rows}>
    {(item) => (
      <Row>
        {(columnKey) => <Cell>{item[columnKey]}</Cell>}
      </Row>
    )}
  </TableBody>
</TableView>
let columns = [
  {
    name: 'Name',
    uid: 'name'
  },
  {
    name: 'Type',
    uid: 'type'
  },
  {
    name:
      'Date Modified',
    uid: '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'
  }
];

<TableView
  aria-label="Example table with dynamic content"
  maxWidth="size-6000"
>
  <TableHeader
    columns={columns}
  >
    {(column) => (
      <Column
        key={column
          .uid}
        align={column
            .uid ===
            'date'
          ? 'end'
          : 'start'}
      >
        {column.name}
      </Column>
    )}
  </TableHeader>
  <TableBody
    items={rows}
  >
    {(item) => (
      <Row>
        {(columnKey) => (
          <Cell>
            {item[
              columnKey
            ]}
          </Cell>
        )}
      </Row>
    )}
  </TableBody>
</TableView>

Layout#

TableViews are often contained within a page layout that defines the size of the table. For example, a page might have a header or toolbar with a TableView below that fills the remaining vertical space. TableView is designed to scroll internally while the column headers remain fixed. Because of this, TableViews should not be placed within a scrollable container.

The example below shows how to use a Flex component to achieve the layout described above. Note the TableView uses the flex prop to fill the remainder of the available space.

<Flex height="size-5000" width="100%" direction="column" gap="size-150">
  <ActionButton alignSelf="start">Add</ActionButton>
  <TableView
    flex
    aria-label="Example table with dynamic content"
  >
    <TableHeader columns={columns}>
      {(column) => (
        <Column
          key={column.id}
        >
          {column.name}
        </Column>
      )}
    </TableHeader>
    <TableBody items={rows}>
      {(item) => (
        <Row>
          {(columnKey) => <Cell>{item[columnKey]}</Cell>}
        </Row>
      )}
    </TableBody>
  </TableView>
</Flex>
<Flex
  height="size-5000"
  width="100%"
  direction="column"
  gap="size-150"
>
  <ActionButton alignSelf="start">Add</ActionButton>
  <TableView
    flex
    aria-label="Example table with dynamic content"
  >
    <TableHeader columns={columns}>
      {(column) => (
        <Column
          key={column.id}
        >
          {column.name}
        </Column>
      )}
    </TableHeader>
    <TableBody items={rows}>
      {(item) => (
        <Row>
          {(columnKey) => <Cell>{item[columnKey]}</Cell>}
        </Row>
      )}
    </TableBody>
  </TableView>
</Flex>
<Flex
  height="size-5000"
  width="100%"
  direction="column"
  gap="size-150"
>
  <ActionButton alignSelf="start">
    Add
  </ActionButton>
  <TableView
    flex
    aria-label="Example table with dynamic content"
  >
    <TableHeader
      columns={columns}
    >
      {(column) => (
        <Column
          key={column
            .id}
        >
          {column.name}
        </Column>
      )}
    </TableHeader>
    <TableBody
      items={rows}
    >
      {(item) => (
        <Row>
          {(columnKey) => (
            <Cell>
              {item[
                columnKey
              ]}
            </Cell>
          )}
        </Row>
      )}
    </TableBody>
  </TableView>
</Flex>

Internationalization#

To internationalize a TableView, all text content within the TableView should be localized. This includes the aria-label provided to the TableView if any. For languages that are read right-to-left (e.g. Hebrew and Arabic), the layout of TableView is automatically flipped.

Labeling#


Accessibility#

An aria-label must be provided to the TableView for accessibility. If the TableView is labeled by a separate element, an aria-labelledby prop must be provided using the id of the labeling element instead.

By default, the first column of the TableView is used as the row header and is announced by assistive technology when navigating through the rows. You can override this behavior by providing the isRowHeader prop to one or more Columns, allowing you to customize which columns should label the rows of the TableView.

The example below applies isRowHeader to the "First Name" and "Last Name" columns so that each row is announced with the person's full name (e.g. "John Doe").

<TableView aria-label="Example table with static contents">
  <TableHeader>
    <Column isRowHeader>First Name</Column>
    <Column isRowHeader>Last Name</Column>
    <Column align="end">Age</Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>John</Cell>
      <Cell>Doe</Cell>
      <Cell>45</Cell>
    </Row>
    <Row>
      <Cell>Jane</Cell>
      <Cell>Doe</Cell>
      <Cell>37</Cell>
    </Row>
    <Row>
      <Cell>Joe</Cell>
      <Cell>Schmoe</Cell>
      <Cell>67</Cell>
    </Row>
  </TableBody>
</TableView>
<TableView aria-label="Example table with static contents">
  <TableHeader>
    <Column isRowHeader>First Name</Column>
    <Column isRowHeader>Last Name</Column>
    <Column align="end">Age</Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>John</Cell>
      <Cell>Doe</Cell>
      <Cell>45</Cell>
    </Row>
    <Row>
      <Cell>Jane</Cell>
      <Cell>Doe</Cell>
      <Cell>37</Cell>
    </Row>
    <Row>
      <Cell>Joe</Cell>
      <Cell>Schmoe</Cell>
      <Cell>67</Cell>
    </Row>
  </TableBody>
</TableView>
<TableView aria-label="Example table with static contents">
  <TableHeader>
    <Column
      isRowHeader
    >
      First Name
    </Column>
    <Column
      isRowHeader
    >
      Last Name
    </Column>
    <Column align="end">
      Age
    </Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>John</Cell>
      <Cell>Doe</Cell>
      <Cell>45</Cell>
    </Row>
    <Row>
      <Cell>Jane</Cell>
      <Cell>Doe</Cell>
      <Cell>37</Cell>
    </Row>
    <Row>
      <Cell>Joe</Cell>
      <Cell>
        Schmoe
      </Cell>
      <Cell>67</Cell>
    </Row>
  </TableBody>
</TableView>

Asynchronous loading#


TableView supports loading data asynchronously, and will display a progress circle reflecting the current load state, set by the loadingState prop. It also supports infinite scrolling to load more data on demand as the user scrolls, via the onLoadMore prop.

This example uses the useAsyncList hook to handle loading the data. See the docs for more information.

import {useAsyncList} from '@react-stately/data';

function AsyncTable() {
  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, 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="example async loading table" height="size-3000">
      <TableHeader columns={columns}>
        {(column) => (
          <Column align={column.key !== 'name' ? 'end' : 'start'}>
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody
        items={list.items}
        loadingState={list.loadingState}
        onLoadMore={list.loadMore}
      >
        {(item) => (
          <Row key={item.name}>{(key) => <Cell>{item[key]}</Cell>}</Row>
        )}
      </TableBody>
    </TableView>
  );
}
import {useAsyncList} from '@react-stately/data';

function AsyncTable() {
  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, 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="example async loading table"
      height="size-3000"
    >
      <TableHeader columns={columns}>
        {(column) => (
          <Column
            align={column.key !== 'name'
              ? 'end'
              : 'start'}
          >
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody
        items={list.items}
        loadingState={list.loadingState}
        onLoadMore={list.loadMore}
      >
        {(item) => (
          <Row key={item.name}>
            {(key) => <Cell>{item[key]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}
import {useAsyncList} from '@react-stately/data';

function AsyncTable() {
  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,
          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="example async loading table"
      height="size-3000"
    >
      <TableHeader
        columns={columns}
      >
        {(column) => (
          <Column
            align={column
                .key !==
                'name'
              ? 'end'
              : 'start'}
          >
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody
        items={list
          .items}
        loadingState={list
          .loadingState}
        onLoadMore={list
          .loadMore}
      >
        {(item) => (
          <Row
            key={item
              .name}
          >
            {(key) => (
              <Cell>
                {item[
                  key
                ]}
              </Cell>
            )}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}

Selection#


By default, TableView doesn't allow row selection but this can be enabled 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 enables multiple selection mode, and uses defaultSelectedKeys to select the rows with keys "2" and "4".

<TableView
  aria-label="Example table with multiple selection"
  selectionMode="multiple"
  defaultSelectedKeys={['2', '4']}
>
  <TableHeader>
    <Column>Name</Column>
    <Column>Type</Column>
    <Column align="end">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>
</TableView>
<TableView
  aria-label="Example table with multiple selection"
  selectionMode="multiple"
  defaultSelectedKeys={['2', '4']}
>
  <TableHeader>
    <Column>Name</Column>
    <Column>Type</Column>
    <Column align="end">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>
</TableView>
<TableView
  aria-label="Example table with multiple selection"
  selectionMode="multiple"
  defaultSelectedKeys={[
    '2',
    '4'
  ]}
>
  <TableHeader>
    <Column>
      Name
    </Column>
    <Column>
      Type
    </Column>
    <Column align="end">
      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>
</TableView>

Controlled selection#

To programmatically control row selection, use the selectedKeys prop paired with the onSelectionChange callback. The key prop from the selected rows will be passed into the callback when the row is pressed, allowing you to update state 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 [selectedKeys, setSelectedKeys] = React.useState(new Set([2]));

  return (
    <TableView
      aria-label="Table with controlled selection"
      selectionMode="multiple"
      selectedKeys={selectedKeys}
      onSelectionChange={setSelectedKeys}
      {...props}
    >
      <TableHeader columns={columns}>
        {(column) => (
          <Column
            key={column.uid}
            align={column.uid === 'level' ? 'end' : 'start'}
          >
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={rows}>
        {(item) => (
          <Row>
            {(columnKey) => <Cell>{item[columnKey]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}
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 [selectedKeys, setSelectedKeys] = React.useState(
    new Set([2])
  );

  return (
    <TableView
      aria-label="Table with controlled selection"
      selectionMode="multiple"
      selectedKeys={selectedKeys}
      onSelectionChange={setSelectedKeys}
      {...props}
    >
      <TableHeader columns={columns}>
        {(column) => (
          <Column
            key={column.uid}
            align={column.uid === 'level'
              ? 'end'
              : 'start'}
          >
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={rows}>
        {(item) => (
          <Row>
            {(columnKey) => <Cell>{item[columnKey]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}
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 [
    selectedKeys,
    setSelectedKeys
  ] = React.useState(
    new Set([2])
  );

  return (
    <TableView
      aria-label="Table with controlled selection"
      selectionMode="multiple"
      selectedKeys={selectedKeys}
      onSelectionChange={setSelectedKeys}
      {...props}
    >
      <TableHeader
        columns={columns}
      >
        {(column) => (
          <Column
            key={column
              .uid}
            align={column
                .uid ===
                'level'
              ? 'end'
              : 'start'}
          >
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody
        items={rows}
      >
        {(item) => (
          <Row>
            {(columnKey) => (
              <Cell>
                {item[
                  columnKey
                ]}
              </Cell>
            )}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}

Single selection#

To limit users to selecting only a single item at a time, selectionMode can be set to single.

// Using the same table as above
<PokemonTable selectionMode="single" />
// Using the same table as above
<PokemonTable selectionMode="single" />
// Using the same table as above
<PokemonTable selectionMode="single" />

Disallow empty selection#

TableView also supports a disallowEmptySelection prop which forces the user to have at least one row in the TableView selected at all times. In this mode, if a single row is selected and the user presses it, it will not be deselected.

// Using the same table as above
<PokemonTable disallowEmptySelection />
// Using the same table as above
<PokemonTable disallowEmptySelection />
// Using the same table as above
<PokemonTable
  disallowEmptySelection
/>

Disabled rows#

You can disable specific rows by providing an array of keys to TableView via the disabledKeys prop. This will prevent rows from being selectable as shown in the example below.

// Using the same table as above
<PokemonTable selectionMode="multiple" disabledKeys={[3]} />
// Using the same table as above
<PokemonTable selectionMode="multiple" disabledKeys={[3]} />
// Using the same table as above
<PokemonTable
  selectionMode="multiple"
  disabledKeys={[3]}
/>

Highlight selection#

By default, TableView uses the checkbox selection style, which includes a column of checkboxes in each row for selection. When the selectionStyle prop is set to "highlight", the checkboxes are hidden, and the selected rows are displayed with a highlighted background instead.

In addition to changing the appearance, the selection behavior also changes depending on the selectionStyle prop. In the default checkbox selection style, clicking, tapping, or pressing the Space or Enter keys toggles selection for the focused row. Using the arrow keys moves focus but does not change selection.

In the highlight selection style, however, clicking a row with the mouse replaces the selection with only that row. Using the arrow keys moves both focus and selection. To select multiple rows, modifier keys such as Ctrl, Cmd, and Shift can be used. To move focus without moving selection, the Ctrl key on Windows or the Option key on macOS can be held while pressing the arrow keys. Holding this modifier while pressing the Space key toggles selection for the focused row, which allows multiple selection of non-contiguous items. On touch screen devices, selection always behaves as toggle since modifier keys may not be available. This behavior emulates native platforms such as macOS and Windows.

<PokemonTable selectionMode="multiple" selectionStyle="highlight" />
<PokemonTable
  selectionMode="multiple"
  selectionStyle="highlight"
/>
<PokemonTable
  selectionMode="multiple"
  selectionStyle="highlight"
/>

Row actions#

TableView may be used in use cases where users can perform actions on rows, such as navigating into items to open them or get more details. In the default checkbox selection style, it is recommended to use a Link component in the first column for navigation.

In the highlight selection style, the onAction prop can be used to enable row actions, which differ depending on the interaction method. When provided, double clicking with a mouse or pressing the Enter key triggers onAction, while single click and the Space key are reserved for selection. On touch devices, onAction becomes the primary tap interaction, and a long press enters into selection mode, which displays checkboxes to perform selection. Deselecting all items exits selection mode and hides the checkboxes. Double clicking matches the behavior of desktop platforms like macOS and Windows, and a separate selection mode matches mobile platforms like iOS and Android.

<PokemonTable
  selectionMode="multiple"
  selectionStyle="highlight"
  onAction={(key) => alert(`Opening item ${key}...`)}
/>
<PokemonTable
  selectionMode="multiple"
  selectionStyle="highlight"
  onAction={(key) => alert(`Opening item ${key}...`)}
/>
<PokemonTable
  selectionMode="multiple"
  selectionStyle="highlight"
  onAction={(key) =>
    alert(
      `Opening item ${key}...`
    )}
/>

Sorting#


TableView 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 TableView accepts a sortDescriptor prop that defines the current column key to sort by and the sort direction (ascending/descending). When the user presses a sortable column header, 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 {useCollator} from '@react-aria/i18n';

function AsyncSortTable() {
  let collator = useCollator({ numeric: true });

  let list = useAsyncList({
    async load({ signal }) {
      let res = await fetch(`https://swapi.py4e.com/api/people/?search`, {
        signal
      });
      let json = await res.json();
      return {
        items: json.results
      };
    },
    async sort({ items, sortDescriptor }) {
      return {
        items: items.sort((a, b) => {
          let first = a[sortDescriptor.column];
          let second = b[sortDescriptor.column];
          let cmp = collator.compare(first, second);
          if (sortDescriptor.direction === 'descending') {
            cmp *= -1;
          }
          return cmp;
        })
      };
    }
  });

  return (
    <TableView
      aria-label="Example table with client side sorting"
      sortDescriptor={list.sortDescriptor}
      onSortChange={list.sort}
      height="size-3000"
    >
      <TableHeader>
        <Column key="name" allowsSorting>Name</Column>
        <Column key="height" allowsSorting>Height</Column>
        <Column key="mass" allowsSorting>Mass</Column>
        <Column key="birth_year" allowsSorting>Birth Year</Column>
      </TableHeader>
      <TableBody
        items={list.items}
        loadingState={list.loadingState}
      >
        {(item) => (
          <Row key={item.name}>
            {(columnKey) => <Cell>{item[columnKey]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}
import {useCollator} from '@react-aria/i18n';

function AsyncSortTable() {
  let collator = useCollator({ numeric: true });

  let list = useAsyncList({
    async load({ signal }) {
      let res = await fetch(
        `https://swapi.py4e.com/api/people/?search`,
        { signal }
      );
      let json = await res.json();
      return {
        items: json.results
      };
    },
    async sort({ items, sortDescriptor }) {
      return {
        items: items.sort((a, b) => {
          let first = a[sortDescriptor.column];
          let second = b[sortDescriptor.column];
          let cmp = collator.compare(first, second);
          if (sortDescriptor.direction === 'descending') {
            cmp *= -1;
          }
          return cmp;
        })
      };
    }
  });

  return (
    <TableView
      aria-label="Example table with client side sorting"
      sortDescriptor={list.sortDescriptor}
      onSortChange={list.sort}
      height="size-3000"
    >
      <TableHeader>
        <Column key="name" allowsSorting>Name</Column>
        <Column key="height" allowsSorting>Height</Column>
        <Column key="mass" allowsSorting>Mass</Column>
        <Column key="birth_year" allowsSorting>
          Birth Year
        </Column>
      </TableHeader>
      <TableBody
        items={list.items}
        loadingState={list.loadingState}
      >
        {(item) => (
          <Row key={item.name}>
            {(columnKey) => <Cell>{item[columnKey]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}
import {useCollator} from '@react-aria/i18n';

function AsyncSortTable() {
  let collator =
    useCollator({
      numeric: true
    });

  let list =
    useAsyncList({
      async load(
        { signal }
      ) {
        let res =
          await fetch(
            `https://swapi.py4e.com/api/people/?search`,
            { signal }
          );
        let json =
          await res
            .json();
        return {
          items:
            json.results
        };
      },
      async sort(
        {
          items,
          sortDescriptor
        }
      ) {
        return {
          items: items
            .sort(
              (a, b) => {
                let first =
                  a[
                    sortDescriptor
                      .column
                  ];
                let second =
                  b[
                    sortDescriptor
                      .column
                  ];
                let cmp =
                  collator
                    .compare(
                      first,
                      second
                    );
                if (
                  sortDescriptor
                    .direction ===
                    'descending'
                ) {
                  cmp *=
                    -1;
                }
                return cmp;
              }
            )
        };
      }
    });

  return (
    <TableView
      aria-label="Example table with client side sorting"
      sortDescriptor={list
        .sortDescriptor}
      onSortChange={list
        .sort}
      height="size-3000"
    >
      <TableHeader>
        <Column
          key="name"
          allowsSorting
        >
          Name
        </Column>
        <Column
          key="height"
          allowsSorting
        >
          Height
        </Column>
        <Column
          key="mass"
          allowsSorting
        >
          Mass
        </Column>
        <Column
          key="birth_year"
          allowsSorting
        >
          Birth Year
        </Column>
      </TableHeader>
      <TableBody
        items={list
          .items}
        loadingState={list
          .loadingState}
      >
        {(item) => (
          <Row
            key={item
              .name}
          >
            {(columnKey) => (
              <Cell>
                {item[
                  columnKey
                ]}
              </Cell>
            )}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}

Props#


TableView props#

NameTypeDefaultDescription
children[ ReactElement<TableHeaderProps<T>>, ReactElement<TableBodyProps<T>> ]The elements that make up the table. Includes the TableHeader, TableBody, Columns, and Rows.
density'compact''regular''spacious''regular'Sets the amount of vertical padding within each cell.
overflowMode'wrap''truncate''truncate'Sets the overflow behavior for the cell contents.
isQuietbooleanWhether the TableView should be displayed with a quiet style.
renderEmptyState() => JSX.ElementSets what the TableView should render when there is no content to display.
disabledKeysIterable<Key>A list of row keys to disable.
selectionModeSelectionModeThe type of selection that is allowed in the collection.
disallowEmptySelectionbooleanWhether the collection allows empty selection.
selectedKeys'all'Iterable<Key>The currently selected keys in the collection (controlled).
defaultSelectedKeys'all'Iterable<Key>The initial selected keys in the collection (uncontrolled).
sortDescriptorSortDescriptorThe current sorted column and direction.
selectionStyle'checkbox''highlight'How selection should be displayed.
Events
NameTypeDefaultDescription
onAction( (key: Key )) => voidHandler that is called when a user performs an action on a row.
onColumnResize( (affectedColumns: {
key: Key,
width: number
}[] )) => void
Handler that is called when a user performs a column resize.
onColumnResizeEnd( (affectedColumns: {
key: Key,
width: number
}[] )) => void
Handler that is called when a column resize ends.
onSelectionChange( (keys: Selection )) => anyHandler that is called when the selection changes.
onSortChange( (descriptor: SortDescriptor )) => anyHandler that is called when the sorted column or direction changes.
Layout
NameTypeDefaultDescription
flexResponsive<stringnumberboolean>When used in a flex layout, specifies how the element will grow or shrink to fit the space available. See MDN.
flexGrowResponsive<number>When used in a flex layout, specifies how the element will grow to fit the space available. See MDN.
flexShrinkResponsive<number>When used in a flex layout, specifies how the element will shrink to fit the space available. See MDN.
flexBasisResponsive<numberstring>When used in a flex layout, specifies the initial main size of the element. See MDN.
alignSelfResponsive<'auto''normal''start''end''center''flex-start''flex-end''self-start''self-end''stretch'>Overrides the alignItems property of a flex or grid container. See MDN.
justifySelfResponsive<'auto''normal''start''end''flex-start''flex-end''self-start''self-end''center''left''right''stretch'>Specifies how the element is justified inside a flex or grid container. See MDN.
orderResponsive<number>The layout order for the element within a flex or grid container. See MDN.
gridAreaResponsive<string>When used in a grid layout, specifies the named grid area that the element should be placed in within the grid. See MDN.
gridColumnResponsive<string>When used in a grid layout, specifies the column the element should be placed in within the grid. See MDN.
gridRowResponsive<string>When used in a grid layout, specifies the row the element should be placed in within the grid. See MDN.
gridColumnStartResponsive<string>When used in a grid layout, specifies the starting column to span within the grid. See MDN.
gridColumnEndResponsive<string>When used in a grid layout, specifies the ending column to span within the grid. See MDN.
gridRowStartResponsive<string>When used in a grid layout, specifies the starting row to span within the grid. See MDN.
gridRowEndResponsive<string>When used in a grid layout, specifies the ending row to span within the grid. See MDN.
Spacing
NameTypeDefaultDescription
marginResponsive<DimensionValue>The margin for all four sides of the element. See MDN.
marginTopResponsive<DimensionValue>The margin for the top side of the element. See MDN.
marginBottomResponsive<DimensionValue>The margin for the bottom side of the element. See MDN.
marginStartResponsive<DimensionValue>The margin for the logical start side of the element, depending on layout direction. See MDN.
marginEndResponsive<DimensionValue>The margin for the logical end side of an element, depending on layout direction. See MDN.
marginXResponsive<DimensionValue>The margin for both the left and right sides of the element. See MDN.
marginYResponsive<DimensionValue>The margin for both the top and bottom sides of the element. See MDN.
Sizing
NameTypeDefaultDescription
widthResponsive<DimensionValue>The width of the element. See MDN.
minWidthResponsive<DimensionValue>The minimum width of the element. See MDN.
maxWidthResponsive<DimensionValue>The maximum width of the element. See MDN.
heightResponsive<DimensionValue>The height of the element. See MDN.
minHeightResponsive<DimensionValue>The minimum height of the element. See MDN.
maxHeightResponsive<DimensionValue>The maximum height of the element. See MDN.
Positioning
NameTypeDefaultDescription
positionResponsive<'static''relative''absolute''fixed''sticky'>Specifies how the element is positioned. See MDN.
topResponsive<DimensionValue>The top position for the element. See MDN.
bottomResponsive<DimensionValue>The bottom position for the element. See MDN.
leftResponsive<DimensionValue>The left position for the element. See MDN. Consider using start instead for RTL support.
rightResponsive<DimensionValue>The right position for the element. See MDN. Consider using start instead for RTL support.
startResponsive<DimensionValue>The logical start position for the element, depending on layout direction. See MDN.
endResponsive<DimensionValue>The logical end position for the element, depending on layout direction. See MDN.
zIndexResponsive<number>The stacking order for the element. See MDN.
isHiddenResponsive<boolean>Hides the element.
Accessibility
NameTypeDefaultDescription
idstringThe element's unique identifier. See MDN.
aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.
Advanced
NameTypeDefaultDescription
UNSAFE_classNamestringSets the CSS className for the element. Only use as a last resort. Use style props instead.
UNSAFE_styleCSSPropertiesSets inline style for the element. Only use as a last resort. Use style props instead.

TableHeader props#

NameTypeDefaultDescription
childrenColumnElement<T>ColumnElement<T>[]ColumnRenderer<T>A list of Column(s) or a function. If the latter, a list of columns must be provided using the columns prop.
columnsT[]A list of table columns.

Column props#

NameTypeDefaultDescription
childrenReactNodeColumnElement<T>ColumnElement<T>[]Static child columns or content to render as the column header.
align'start''center''end''start'The alignment of the column's contents relative to its allotted width.
showDividerbooleanWhether the column should render a divider between it and the next column.
hideHeaderboolean

Whether the column should hide its header text. A tooltip with the column's header text will be displayed when the column header is focused instead. Note that this prop is specifically for columns that contain ActionButtons in place of text content.

titleReactNodeRendered contents of the column if children contains child columns.
childColumnsT[]A list of child columns used when dynamically rendering nested child columns.
defaultWidthnumberstringThe default width of the column.
allowsResizingbooleanWhether the column allows resizing.
allowsSortingbooleanWhether the column allows sorting.
isRowHeaderbooleanWhether a column is a row header and should be announced by assistive technology during row navigation.
textValuestringA string representation of the column's contents, used for accessibility announcements.
Sizing
NameTypeDefaultDescription
widthnumberstringThe width of the column.
minWidthnumberstringThe minimum width of the column.
maxWidthnumberstringThe maximum width of the column.

TableBody props#

NameTypeDefaultDescription
childrenCollectionChildren<T>The contents of the table body. Supports static items or a function for dynamic rendering.
itemsIterable<T>A list of row objects in the table body used when dynamically rendering rows.
loadingStateLoadingStateThe current loading state of the table.
Events
NameTypeDefaultDescription
onLoadMore() => anyHandler that is called when more items should be loaded, e.g. while scrolling near the bottom.

Row props#

NameTypeDefaultDescription
childrenCellElementCellElement[]CellRendererRendered contents of the row or row child items.
textValuestringA string representation of the row's contents, used for features like typeahead.

Cell props#

NameTypeDefaultDescription
childrenReactNodeThe contents of the cell.
textValuestringA string representation of the cell's contents, used for features like typeahead.

Visual options#


Column alignment#

View guidelines

<TableView aria-label="Example table for column alignment">
  <TableHeader>
    <Column align="start">Name</Column>
    <Column align="center">Type</Column>
    <Column align="end">Size</Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>2021406_Proposal</Cell>
      <Cell>PDF</Cell>
      <Cell>86 KB</Cell>
    </Row>
    <Row>
      <Cell>Budget Template</Cell>
      <Cell>XLS</Cell>
      <Cell>120 KB</Cell>
    </Row>
    <Row>
      <Cell>Onboarding</Cell>
      <Cell>PPT</Cell>
      <Cell>472 KB</Cell>
    </Row>
    <Row>
      <Cell>Welcome</Cell>
      <Cell>TXT</Cell>
      <Cell>24 KB</Cell>
    </Row>
  </TableBody>
</TableView>
<TableView aria-label="Example table for column alignment">
  <TableHeader>
    <Column align="start">Name</Column>
    <Column align="center">Type</Column>
    <Column align="end">Size</Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>2021406_Proposal</Cell>
      <Cell>PDF</Cell>
      <Cell>86 KB</Cell>
    </Row>
    <Row>
      <Cell>Budget Template</Cell>
      <Cell>XLS</Cell>
      <Cell>120 KB</Cell>
    </Row>
    <Row>
      <Cell>Onboarding</Cell>
      <Cell>PPT</Cell>
      <Cell>472 KB</Cell>
    </Row>
    <Row>
      <Cell>Welcome</Cell>
      <Cell>TXT</Cell>
      <Cell>24 KB</Cell>
    </Row>
  </TableBody>
</TableView>
<TableView aria-label="Example table for column alignment">
  <TableHeader>
    <Column align="start">
      Name
    </Column>
    <Column align="center">
      Type
    </Column>
    <Column align="end">
      Size
    </Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>
        2021406_Proposal
      </Cell>
      <Cell>PDF</Cell>
      <Cell>
        86 KB
      </Cell>
    </Row>
    <Row>
      <Cell>
        Budget Template
      </Cell>
      <Cell>XLS</Cell>
      <Cell>
        120 KB
      </Cell>
    </Row>
    <Row>
      <Cell>
        Onboarding
      </Cell>
      <Cell>PPT</Cell>
      <Cell>
        472 KB
      </Cell>
    </Row>
    <Row>
      <Cell>
        Welcome
      </Cell>
      <Cell>TXT</Cell>
      <Cell>
        24 KB
      </Cell>
    </Row>
  </TableBody>
</TableView>

Column widths#

Columns support three different width props: minWidth, width, and maxWidth. Each of these props accepts fixed values or percentages. TableView prioritizes columns with defined widths and divides the remaining space evenly amongst the other columns.

<TableView aria-label="Example table for column widths">
  <TableHeader>
    <Column maxWidth={300} align="start">Name</Column>
    <Column width={80}>Type</Column>
    <Column minWidth={100} align="end">Size</Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>2021406_Proposal</Cell>
      <Cell>PDF</Cell>
      <Cell>86 KB</Cell>
    </Row>
    <Row>
      <Cell>Budget Template</Cell>
      <Cell>XLS</Cell>
      <Cell>120 KB</Cell>
    </Row>
    <Row>
      <Cell>Onboarding</Cell>
      <Cell>PPT</Cell>
      <Cell>472 KB</Cell>
    </Row>
    <Row>
      <Cell>Welcome</Cell>
      <Cell>TXT</Cell>
      <Cell>24 KB</Cell>
    </Row>
  </TableBody>
</TableView>
<TableView aria-label="Example table for column widths">
  <TableHeader>
    <Column maxWidth={300} align="start">Name</Column>
    <Column width={80}>Type</Column>
    <Column minWidth={100} align="end">Size</Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>2021406_Proposal</Cell>
      <Cell>PDF</Cell>
      <Cell>86 KB</Cell>
    </Row>
    <Row>
      <Cell>Budget Template</Cell>
      <Cell>XLS</Cell>
      <Cell>120 KB</Cell>
    </Row>
    <Row>
      <Cell>Onboarding</Cell>
      <Cell>PPT</Cell>
      <Cell>472 KB</Cell>
    </Row>
    <Row>
      <Cell>Welcome</Cell>
      <Cell>TXT</Cell>
      <Cell>24 KB</Cell>
    </Row>
  </TableBody>
</TableView>
<TableView aria-label="Example table for column widths">
  <TableHeader>
    <Column
      maxWidth={300}
      align="start"
    >
      Name
    </Column>
    <Column width={80}>
      Type
    </Column>
    <Column
      minWidth={100}
      align="end"
    >
      Size
    </Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>
        2021406_Proposal
      </Cell>
      <Cell>PDF</Cell>
      <Cell>
        86 KB
      </Cell>
    </Row>
    <Row>
      <Cell>
        Budget Template
      </Cell>
      <Cell>XLS</Cell>
      <Cell>
        120 KB
      </Cell>
    </Row>
    <Row>
      <Cell>
        Onboarding
      </Cell>
      <Cell>PPT</Cell>
      <Cell>
        472 KB
      </Cell>
    </Row>
    <Row>
      <Cell>
        Welcome
      </Cell>
      <Cell>TXT</Cell>
      <Cell>
        24 KB
      </Cell>
    </Row>
  </TableBody>
</TableView>

Column dividers#

View guidelines

<TableView aria-label="Example table for column dividers">
  <TableHeader>
    <Column align="start" showDivider>Name</Column>
    <Column showDivider>Type</Column>
    <Column align="end" showDivider>Size</Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>2021406_Proposal</Cell>
      <Cell>PDF</Cell>
      <Cell>86 KB</Cell>
    </Row>
    <Row>
      <Cell>Budget Template</Cell>
      <Cell>XLS</Cell>
      <Cell>120 KB</Cell>
    </Row>
    <Row>
      <Cell>Onboarding</Cell>
      <Cell>PPT</Cell>
      <Cell>472 KB</Cell>
    </Row>
    <Row>
      <Cell>Welcome</Cell>
      <Cell>TXT</Cell>
      <Cell>24 KB</Cell>
    </Row>
  </TableBody>
</TableView>
<TableView aria-label="Example table for column dividers">
  <TableHeader>
    <Column align="start" showDivider>Name</Column>
    <Column showDivider>Type</Column>
    <Column align="end" showDivider>Size</Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>2021406_Proposal</Cell>
      <Cell>PDF</Cell>
      <Cell>86 KB</Cell>
    </Row>
    <Row>
      <Cell>Budget Template</Cell>
      <Cell>XLS</Cell>
      <Cell>120 KB</Cell>
    </Row>
    <Row>
      <Cell>Onboarding</Cell>
      <Cell>PPT</Cell>
      <Cell>472 KB</Cell>
    </Row>
    <Row>
      <Cell>Welcome</Cell>
      <Cell>TXT</Cell>
      <Cell>24 KB</Cell>
    </Row>
  </TableBody>
</TableView>
<TableView aria-label="Example table for column dividers">
  <TableHeader>
    <Column
      align="start"
      showDivider
    >
      Name
    </Column>
    <Column
      showDivider
    >
      Type
    </Column>
    <Column
      align="end"
      showDivider
    >
      Size
    </Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>
        2021406_Proposal
      </Cell>
      <Cell>PDF</Cell>
      <Cell>
        86 KB
      </Cell>
    </Row>
    <Row>
      <Cell>
        Budget Template
      </Cell>
      <Cell>XLS</Cell>
      <Cell>
        120 KB
      </Cell>
    </Row>
    <Row>
      <Cell>
        Onboarding
      </Cell>
      <Cell>PPT</Cell>
      <Cell>
        472 KB
      </Cell>
    </Row>
    <Row>
      <Cell>
        Welcome
      </Cell>
      <Cell>TXT</Cell>
      <Cell>
        24 KB
      </Cell>
    </Row>
  </TableBody>
</TableView>

Hide header#

Individual column headers can be hidden by providing the hideHeader prop to the Column. A tooltip is rendered when the column header is focused to compensate for the lack of a visual title. Note that the hideHeader prop is specifically intended for columns that contain ActionButtons instead of text content.

function TableExample(props) {
  let columns = [
    { name: 'First Name', key: 'firstName' },
    { name: 'Last Name', key: 'lastName' },
    { name: 'Add Info', key: 'addInfo' },
    { name: 'Age', key: 'age' }
  ];

  let rows = [
    { id: '1', firstName: 'John', lastName: 'Doe', age: '45' },
    { id: '2', firstName: 'Jane', lastName: 'Doe', age: '37' },
    { id: '3', firstName: 'Joe', lastName: 'Schmoe', age: '67' },
    { id: '4', firstName: 'Joe', lastName: 'Bloggs', age: '12' },
    {
      id: '5',
      firstName: 'Taylor',
      lastName: 'Rodriguez Lloyd-Atkinson',
      age: '83'
    }
  ];

  return (
    <TableView
      aria-label="Example table with hidden headers"
      maxWidth="size-6000"
      {...props}
    >
      <TableHeader columns={columns}>
        {(column) => (
          <Column
            hideHeader={column.key === 'addInfo'}
            align={column.key === 'age' ? 'end' : 'start'}
            showDivider={column.key === 'addInfo'}
          >
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={rows}>
        {(item) => (
          <Row key={item.id}>
            {(key) =>
              key === 'addInfo'
                ? (
                  <Cell>
                    <ActionButton aria-label="Add Info" isQuiet>
                      <Add />
                    </ActionButton>
                  </Cell>
                )
                : <Cell>{item[key]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}
function TableExample(props) {
  let columns = [
    { name: 'First Name', key: 'firstName' },
    { name: 'Last Name', key: 'lastName' },
    { name: 'Add Info', key: 'addInfo' },
    { name: 'Age', key: 'age' }
  ];

  let rows = [
    {
      id: '1',
      firstName: 'John',
      lastName: 'Doe',
      age: '45'
    },
    {
      id: '2',
      firstName: 'Jane',
      lastName: 'Doe',
      age: '37'
    },
    {
      id: '3',
      firstName: 'Joe',
      lastName: 'Schmoe',
      age: '67'
    },
    {
      id: '4',
      firstName: 'Joe',
      lastName: 'Bloggs',
      age: '12'
    },
    {
      id: '5',
      firstName: 'Taylor',
      lastName: 'Rodriguez Lloyd-Atkinson',
      age: '83'
    }
  ];

  return (
    <TableView
      aria-label="Example table with hidden headers"
      maxWidth="size-6000"
      {...props}
    >
      <TableHeader columns={columns}>
        {(column) => (
          <Column
            hideHeader={column.key === 'addInfo'}
            align={column.key === 'age' ? 'end' : 'start'}
            showDivider={column.key === 'addInfo'}
          >
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={rows}>
        {(item) => (
          <Row key={item.id}>
            {(key) =>
              key === 'addInfo'
                ? (
                  <Cell>
                    <ActionButton
                      aria-label="Add Info"
                      isQuiet
                    >
                      <Add />
                    </ActionButton>
                  </Cell>
                )
                : <Cell>{item[key]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}
function TableExample(
  props
) {
  let columns = [
    {
      name: 'First Name',
      key: 'firstName'
    },
    {
      name: 'Last Name',
      key: 'lastName'
    },
    {
      name: 'Add Info',
      key: 'addInfo'
    },
    {
      name: 'Age',
      key: 'age'
    }
  ];

  let rows = [
    {
      id: '1',
      firstName: 'John',
      lastName: 'Doe',
      age: '45'
    },
    {
      id: '2',
      firstName: 'Jane',
      lastName: 'Doe',
      age: '37'
    },
    {
      id: '3',
      firstName: 'Joe',
      lastName: 'Schmoe',
      age: '67'
    },
    {
      id: '4',
      firstName: 'Joe',
      lastName: 'Bloggs',
      age: '12'
    },
    {
      id: '5',
      firstName:
        'Taylor',
      lastName:
        'Rodriguez Lloyd-Atkinson',
      age: '83'
    }
  ];

  return (
    <TableView
      aria-label="Example table with hidden headers"
      maxWidth="size-6000"
      {...props}
    >
      <TableHeader
        columns={columns}
      >
        {(column) => (
          <Column
            hideHeader={column
              .key ===
              'addInfo'}
            align={column
                .key ===
                'age'
              ? 'end'
              : 'start'}
            showDivider={column
              .key ===
              'addInfo'}
          >
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody
        items={rows}
      >
        {(item) => (
          <Row
            key={item.id}
          >
            {(key) =>
              key ===
                  'addInfo'
                ? (
                  <Cell>
                    <ActionButton
                      aria-label="Add Info"
                      isQuiet
                    >
                      <Add />
                    </ActionButton>
                  </Cell>
                )
                : (
                  <Cell>
                    {item[
                      key
                    ]}
                  </Cell>
                )}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}

Quiet#

View guidelines

// Using same setup as hide header example
<TableExample isQuiet />
// Using same setup as hide header example
<TableExample isQuiet />
// Using same setup as hide header example
<TableExample
  isQuiet
/>

Density#

The amount of vertical padding that each row contains can be modified by providing the density prop.

// Using same setup as hide header example
<Flex direction="column" gap="size-300">
  <TableExample density="compact" />
  <TableExample density="spacious" />
</Flex>
// Using same setup as hide header example
<Flex direction="column" gap="size-300">
  <TableExample density="compact" />
  <TableExample density="spacious" />
</Flex>
// Using same setup as hide header example
<Flex
  direction="column"
  gap="size-300"
>
  <TableExample density="compact" />
  <TableExample density="spacious" />
</Flex>

Overflow mode#

By default, text content that overflows its table cell will be truncated. You can have it wrap instead by passing overflowMode="wrap" to the TableView.

// Using same setup as hide header example
<TableExample overflowMode="wrap" />
// Using same setup as hide header example
<TableExample overflowMode="wrap" />
// Using same setup as hide header example
<TableExample overflowMode="wrap" />

Empty state#

Use the renderEmptyState prop to customize what the TableView will display if there are no rows provided.

import {Content} from '@react-spectrum/view';
import {IllustratedMessage} from '@react-spectrum/illustratedmessage';
import NotFound from '@spectrum-icons/illustrations/NotFound';
import {Heading} from '@react-spectrum/text';

function renderEmptyState() {
  return (
    <IllustratedMessage>
      <NotFound />
      <Heading>No results</Heading>
      <Content>No results found</Content>
    </IllustratedMessage>
  );
}

<TableView
  aria-label="Example table for empty state"
  height="size-3000"
  renderEmptyState={renderEmptyState}
>
  <TableHeader>
    <Column>Name</Column>
    <Column>Type</Column>
    <Column>Size</Column>
  </TableHeader>
  <TableBody>
    {[]}
  </TableBody>
</TableView>
import {Content} from '@react-spectrum/view';
import {IllustratedMessage} from '@react-spectrum/illustratedmessage';
import NotFound from '@spectrum-icons/illustrations/NotFound';
import {Heading} from '@react-spectrum/text';

function renderEmptyState() {
  return (
    <IllustratedMessage>
      <NotFound />
      <Heading>No results</Heading>
      <Content>No results found</Content>
    </IllustratedMessage>
  );
}

<TableView
  aria-label="Example table for empty state"
  height="size-3000"
  renderEmptyState={renderEmptyState}
>
  <TableHeader>
    <Column>Name</Column>
    <Column>Type</Column>
    <Column>Size</Column>
  </TableHeader>
  <TableBody>
    {[]}
  </TableBody>
</TableView>
import {Content} from '@react-spectrum/view';
import {IllustratedMessage} from '@react-spectrum/illustratedmessage';
import NotFound from '@spectrum-icons/illustrations/NotFound';
import {Heading} from '@react-spectrum/text';

function renderEmptyState() {
  return (
    <IllustratedMessage>
      <NotFound />
      <Heading>
        No results
      </Heading>
      <Content>
        No results found
      </Content>
    </IllustratedMessage>
  );
}

<TableView
  aria-label="Example table for empty state"
  height="size-3000"
  renderEmptyState={renderEmptyState}
>
  <TableHeader>
    <Column>
      Name
    </Column>
    <Column>
      Type
    </Column>
    <Column>
      Size
    </Column>
  </TableHeader>
  <TableBody>
    {[]}
  </TableBody>
</TableView>

Nested columns#

TableView supports nesting columns, allowing you to create column groups, or "tiered" column headers. Data for the leaf columns appears in each row of the table body.

<TableView aria-label="Example table for nested columns">
  <TableHeader>
    <Column title="Name">
      <Column isRowHeader>First Name</Column>
      <Column isRowHeader>Last Name</Column>
    </Column>
    <Column title="Information">
      <Column>Age</Column>
      <Column>Birthday</Column>
    </Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>Sam</Cell>
      <Cell>Smith</Cell>
      <Cell>36</Cell>
      <Cell>May 3</Cell>
    </Row>
    <Row>
      <Cell>Julia</Cell>
      <Cell>Jones</Cell>
      <Cell>24</Cell>
      <Cell>February 10</Cell>
    </Row>
    <Row>
      <Cell>Peter</Cell>
      <Cell>Parker</Cell>
      <Cell>28</Cell>
      <Cell>September 7</Cell>
    </Row>
    <Row>
      <Cell>Bruce</Cell>
      <Cell>Wayne</Cell>
      <Cell>32</Cell>
      <Cell>December 18</Cell>
    </Row>
  </TableBody>
</TableView>
<TableView aria-label="Example table for nested columns">
  <TableHeader>
    <Column title="Name">
      <Column isRowHeader>First Name</Column>
      <Column isRowHeader>Last Name</Column>
    </Column>
    <Column title="Information">
      <Column>Age</Column>
      <Column>Birthday</Column>
    </Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>Sam</Cell>
      <Cell>Smith</Cell>
      <Cell>36</Cell>
      <Cell>May 3</Cell>
    </Row>
    <Row>
      <Cell>Julia</Cell>
      <Cell>Jones</Cell>
      <Cell>24</Cell>
      <Cell>February 10</Cell>
    </Row>
    <Row>
      <Cell>Peter</Cell>
      <Cell>Parker</Cell>
      <Cell>28</Cell>
      <Cell>September 7</Cell>
    </Row>
    <Row>
      <Cell>Bruce</Cell>
      <Cell>Wayne</Cell>
      <Cell>32</Cell>
      <Cell>December 18</Cell>
    </Row>
  </TableBody>
</TableView>
<TableView aria-label="Example table for nested columns">
  <TableHeader>
    <Column title="Name">
      <Column
        isRowHeader
      >
        First Name
      </Column>
      <Column
        isRowHeader
      >
        Last Name
      </Column>
    </Column>
    <Column title="Information">
      <Column>
        Age
      </Column>
      <Column>
        Birthday
      </Column>
    </Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>Sam</Cell>
      <Cell>
        Smith
      </Cell>
      <Cell>36</Cell>
      <Cell>
        May 3
      </Cell>
    </Row>
    <Row>
      <Cell>
        Julia
      </Cell>
      <Cell>
        Jones
      </Cell>
      <Cell>24</Cell>
      <Cell>
        February 10
      </Cell>
    </Row>
    <Row>
      <Cell>
        Peter
      </Cell>
      <Cell>
        Parker
      </Cell>
      <Cell>28</Cell>
      <Cell>
        September 7
      </Cell>
    </Row>
    <Row>
      <Cell>
        Bruce
      </Cell>
      <Cell>
        Wayne
      </Cell>
      <Cell>32</Cell>
      <Cell>
        December 18
      </Cell>
    </Row>
  </TableBody>
</TableView>

Nested columns can also be defined dynamically using the function syntax and the childColumns prop. The following example is the same as the example above, but defined dynamically.

let columns = [
  {name: 'Name', key: 'name', children: [
    {name: 'First Name', key: 'first', isRowHeader: true},
    {name: 'Last Name', key: 'last', isRowHeader: true}
  ]},
  {name: 'Information', key: 'info', children: [
    {name: 'Age', key: 'age'},
    {name: 'Birthday', key: 'birthday'}
  ]}
];

let rows = [
  {id: 1, first: 'Sam', last: 'Smith', age: 36, birthday: 'May 3'},
  {id: 2, first: 'Julia', last: 'Jones', age: 24, birthday: 'February 10'},
  {id: 3, first: 'Peter', last: 'Parker', age: 28, birthday: 'September 7'},
  {id: 4, first: 'Bruce', last: 'Wayne', age: 32, birthday: 'December 18'}
];

<TableView aria-label="Example table for nested columns">
  <TableHeader columns={columns}>
    {column => (
      <Column isRowHeader={column.isRowHeader} childColumns={column.children}>
        {column.name}
      </Column>
    )}
  </TableHeader>
  <TableBody items={rows}>
    {item => (
      <Row>
        {columnKey => <Cell>{item[columnKey]}</Cell>}
      </Row>
    )}
  </TableBody>
</TableView>
let columns = [
  {
    name: 'Name',
    key: 'name',
    children: [
      {
        name: 'First Name',
        key: 'first',
        isRowHeader: true
      },
      { name: 'Last Name', key: 'last', isRowHeader: true }
    ]
  },
  {
    name: 'Information',
    key: 'info',
    children: [
      { name: 'Age', key: 'age' },
      { name: 'Birthday', key: 'birthday' }
    ]
  }
];

let rows = [
  {
    id: 1,
    first: 'Sam',
    last: 'Smith',
    age: 36,
    birthday: 'May 3'
  },
  {
    id: 2,
    first: 'Julia',
    last: 'Jones',
    age: 24,
    birthday: 'February 10'
  },
  {
    id: 3,
    first: 'Peter',
    last: 'Parker',
    age: 28,
    birthday: 'September 7'
  },
  {
    id: 4,
    first: 'Bruce',
    last: 'Wayne',
    age: 32,
    birthday: 'December 18'
  }
];

<TableView aria-label="Example table for nested columns">
  <TableHeader columns={columns}>
    {(column) => (
      <Column
        isRowHeader={column.isRowHeader}
        childColumns={column.children}
      >
        {column.name}
      </Column>
    )}
  </TableHeader>
  <TableBody items={rows}>
    {(item) => (
      <Row>
        {(columnKey) => <Cell>{item[columnKey]}</Cell>}
      </Row>
    )}
  </TableBody>
</TableView>
let columns = [
  {
    name: 'Name',
    key: 'name',
    children: [
      {
        name:
          'First Name',
        key: 'first',
        isRowHeader: true
      },
      {
        name:
          'Last Name',
        key: 'last',
        isRowHeader: true
      }
    ]
  },
  {
    name: 'Information',
    key: 'info',
    children: [
      {
        name: 'Age',
        key: 'age'
      },
      {
        name: 'Birthday',
        key: 'birthday'
      }
    ]
  }
];

let rows = [
  {
    id: 1,
    first: 'Sam',
    last: 'Smith',
    age: 36,
    birthday: 'May 3'
  },
  {
    id: 2,
    first: 'Julia',
    last: 'Jones',
    age: 24,
    birthday:
      'February 10'
  },
  {
    id: 3,
    first: 'Peter',
    last: 'Parker',
    age: 28,
    birthday:
      'September 7'
  },
  {
    id: 4,
    first: 'Bruce',
    last: 'Wayne',
    age: 32,
    birthday:
      'December 18'
  }
];

<TableView aria-label="Example table for nested columns">
  <TableHeader
    columns={columns}
  >
    {(column) => (
      <Column
        isRowHeader={column
          .isRowHeader}
        childColumns={column
          .children}
      >
        {column.name}
      </Column>
    )}
  </TableHeader>
  <TableBody
    items={rows}
  >
    {(item) => (
      <Row>
        {(columnKey) => (
          <Cell>
            {item[
              columnKey
            ]}
          </Cell>
        )}
      </Row>
    )}
  </TableBody>
</TableView>