Skip to content

Table

Tables are used for displaying and organizing tabular data. Our Table component is a modularized implementation of a native HTML <table>.
Figma logo
A Table with two columns showing a list of employees and the hours worked with pagination along the bottom
Table and its sub-components
Anatomy of the Table component
ElementPurpose
CaptionUsing a caption is always advised. It serves as a label for the component and helps screen readers interpret the content. Caption can be visually hidden from the UI using visuallyHidden.
HeaderIncludes row headers for each column, which can be sortable.
Cell contentContent can be configured and formatted depending on the type of content being displayed. See the alignment section to learn more about formatting conventions.
Footer/PaginationPagination lives in the Footer content area and enables users to customize the number of results per page and cycle through data page by page or jump to the first or last page. Pagination should always be used with Tables unless data is static (not populated by user input). Footers can also be used to display totals of column values.
  • Use Skeleton for data sets that are not immediately available to the user on the initial page load
  • Use a large Spinner for paging between results if the data will not immediately be available (> 100ms wait time)
  • Avoid using a loader if the Table data is immediately available to avoid perceived performance issues

Examples

In this example, we build the structure of the Table and add Skeleton as the content.aria-busy on the Table component indicates to screen readers that the content is loading.

We suggest Spinner when paging between paginated table data that takes >1s to load.

In this non-functioning example, we use a few techniques to achieve a loading state. The styles are written inline for illustrative purposes, but would more likely be done with custom CSS.

  • Set aria-busy on TableBody to indicate to screen readers that the page is waiting for data
  • Lower the opacity on TableBody as a visual indicator of state change
  • Disable pointer-events on the TableSection to avoid interaction while the data loads
  • Position a Spinner over the center of the TableSection

Note: this functionality is available out of the box with DataGrid. It may be easier to use DataGrid instead, but keep in mind that it is a heavier weight component and its role as a grid is semantically different than displaying tabular data.

  • TableSection is used as a wrapper around a table whose height is contained; it serves as a tab stop for keyboard users and allows them to scroll through the table with the and keys
    • Provide context to screen readers using an aria-label or, better yet, associate the TableCaption to the TableSection using aria-labelledby
  • Cell content can force a table column to be wider than its specified value
    • Be sure cell content can flex as necessary, adding <wbr> breaks if necessary to instruct the browser where it can safely wrap text content

This example demonstrates a basic table with the first column sized to 25%.The TableCaption can be hidden using visuallyHidden.

This example demonstrates the use of TableSection to constrain the height of the table. The TableHeader remains visible during scrolling by using position="sticky".

This example demonstrates a sortable table. Workbench does not sort the data itself; an external function must sort the data, which can then be mapped into the TableBody. Box is used to contain the width of the table.

The scope prop is used to tell screen readers which row or column a header is describing. This example illustrates both colgroup and rowgroup.

The following sub-components have props assigned to them. All sub-components not mentioned here accept only children as their content.

TableHeader props
React props
NameTypeDefaultDescription
children  RequiredReactNodeThe content of the TableHeader, generally a series of TableHeaderCell
position  staticstickystaticFollowing CSS positioning rules, static headers will scroll with the page and sticky headers will be fixed to the top of a table whose height is contained
TableHeaderCell props
React props
NameTypeDefaultDescription
align  endcenterstartjustifySets the text-align property
backgroundColor  stringSets the background color of the cell using Workbench color token syntax, e.g. salt.400
children  RequiredReactNodeThe content of the <th> cell
color  stringSets the text color of the cell using Workbench color token syntax, e.g. salt.800
valign  baselinebottomtopmiddleSets the vertical-align property
width  stringnumberEstablishes the width of the cell. Use percentages or fractions to ensure responsive behavior, e.g.: width={1 / 4}.
TableHeaderCellSortable props
React props
NameTypeDefaultDescription
align  endcenterstartjustifySets the text-align property
backgroundColor  stringSets the background color of the cell using Workbench color token syntax, e.g. salt.400
children  RequiredReactNodeThe content of the <th> cell
color  stringSets the text color of the cell using Workbench color token syntax, e.g. salt.800
valign  baselinebottomtopmiddleSets the vertical-align property
width  stringnumberEstablishes the width of the cell. Use percentages or fractions to ensure responsive behavior, e.g.: width={1 / 4}.
scope  rowrowgroupcolcolgroupTells screen readers exactly what cells the header is a header for
sort  noneascendingdescendingothernoneSets thearia-sort value and associated icon. Workbench does not sort the data itself; an external function must sort the data, which can then be mapped
TableDataCell props
React props
NameTypeDefaultDescription
align  endcenterstartjustifySets the text-align property
backgroundColor  stringSets the background color of the cell using Workbench color token syntax, e.g. salt.400
children  RequiredReactNodeThe content of the <td> cell
color  stringSets the text color of the cell using Workbench color token syntax, e.g. salt.800
valign  baselinebottomtopmiddleSets the vertical-align property
TableFooter props
React props
NameTypeDefaultDescription
children  RequiredReactNodeThe content of the TableFooter, generally a series of TableDataCell
position  staticstickystaticFollowing CSS positioning rules, static headers will scroll with the page and sticky headers will be fixed to the top of a table whose height is contained
  • Table captions are required and should provide context about the data in the table. Reading through tabular data is time consuming, so a caption helps users decide if the content is relevant to them.

Note: cells within Table aren’t focusable unless they contain an interactive element like a Button, Link, or IconButton.

Navigating a Table with a keyboard
KeysAction
tab + space, tab + returnToggle sort direction on sortable table headers
tab, shift + tabNavigate between pagination controls
space or returnTrigger action on pagination control buttons
space, , , returnOpen/close, cycle through page results control, make selection

The following testing snippet(s) offer suggestions for testing the component using React Testing LibraryExternal link with occasional help from JestExternal link.

render(
<>
<TableSection aria-labelledby="employee-hours">
<Table>
<TableCaption id="employee-hours">Billable hours</TableCaption>
<TableHeader>
<TableRow>
<TableHeaderCell align="start">Full name</TableHeaderCell>
<TableHeaderCellSortable sort="descending" align="end">
Hours
</TableHeaderCellSortable>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableHeaderCell scope="row">Hannah Ardent</TableHeaderCell>
<TableDataCell align="end">40</TableDataCell>
</TableRow>
<TableRow>
<TableHeaderCell scope="row">Isaiah Berlin</TableHeaderCell>
<TableDataCell align="end">32</TableDataCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableHeaderCell scope="row">Total billable hours</TableHeaderCell>
<TableDataCell align="end">72</TableDataCell>
</TableRow>
</TableFooter>
</Table>
</TableSection>
<TablePagination
aria-label="Billable hours table"
pageSize={10}
previousDisabled
nextDisabled
onPageChange={jest.fn()}
onPageSizeChange={jest.fn()}
/>
</>,
);
// TableSection
const section = screen.getByRole('region', {
name: 'Billable hours',
});
// TableSection -> Table
const table = within(section).getByRole('table', {
name: 'Billable hours',
});
// TableSection -> Table -> TableRow
const [headerRow, hannahRow, isaiahRow, footerRow] = within(table).getAllByRole('row');
// TableSection -> Table -> TableRow -> TableHeaderCell
const [fullNameHeaderCell, billableHoursHeaderCell] = within(headerRow).getAllByRole(
'columnheader',
);
expect(fullNameHeaderCell).toHaveAccessibleName('Full name');
expect(billableHoursHeaderCell).toHaveAccessibleName('Hours');
expect(billableHoursHeaderCell).toHaveAttribute('aria-sort', 'descending');
// TableSection -> Table -> TableRow -> TableHeaderCell
const hannahFullNameCell = within(hannahRow).getByRole('rowheader', {
name: 'Hannah Ardent',
});
// TableSection -> Table -> TableRow -> TableHeaderCell
const isaiahFullNameCell = within(isaiahRow).getByRole('rowheader', {
name: 'Isaiah Berlin',
});
expect(hannahFullNameCell).toBeInTheDocument();
expect(isaiahFullNameCell).toBeInTheDocument();
// TableSection -> Table -> TableRow -> TableDataCell
const [hannahHoursCell] = within(hannahRow).getAllByRole('cell');
expect(hannahHoursCell).toHaveAccessibleName('40');
// TableSection -> Table -> TableRow -> TableDataCell
const [isaiahHoursCell] = within(isaiahRow).getAllByRole('cell');
expect(isaiahHoursCell).toHaveAccessibleName('32');
// TableSection -> Table -> TableRow -> TableHeaderCell
const totalBillableHoursHeaderCell = within(footerRow).getByRole('rowheader', {
name: 'Total billable hours',
});
expect(totalBillableHoursHeaderCell).toBeInTheDocument();
// TableSection -> Table -> TableRow -> TableDataCell
const [totalBillableHoursCell] = within(footerRow).getAllByRole('cell');
expect(totalBillableHoursCell).toHaveAccessibleName('72');
// TablePagination
const tablePagination = screen.getByRole('navigation', {
name: 'Billable hours table',
});
// TablePagination -> Select
const pageSizeSelect = within(tablePagination).getByRole('combobox', {
name: 'Per page:',
});
expect(pageSizeSelect).toHaveDisplayValue('10');
// TablePagination -> Button
const firstPageButton = within(tablePagination).getByRole('button', { name: 'First page' });
const previousPageButton = within(tablePagination).getByRole('button', {
name: 'Previous page',
});
const nextPageButton = within(tablePagination).getByRole('button', { name: 'Next page' });
const lastPageButton = within(tablePagination).getByRole('button', { name: 'Last page' });
expect(firstPageButton).toBeDisabled();
expect(previousPageButton).toBeDisabled();
expect(nextPageButton).toBeDisabled();
expect(lastPageButton).toBeDisabled();