Table
<table>
.Element | Purpose |
---|---|
Caption | Using 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. |
Header | Includes row headers for each column, which can be sortable. |
Cell content | Content can be configured and formatted depending on the type of content being displayed. See the alignment section to learn more about formatting conventions. |
Footer | Pagination 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
onTableBody
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 theTableSection
to avoid interaction while the data loads - Position a
Spinner
over the center of theTableSection
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 theTableCaption
to theTableSection
usingaria-labelledby
- Provide context to screen readers using an
- 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
- Be sure cell content can flex as necessary, adding
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.
Name | Type | Default | Description |
---|---|---|---|
children Required | ReactNode | The content of the TableHeader , generally a series of TableHeaderCell | |
position | static sticky | static | Following 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 |
Name | Type | Default | Description |
---|---|---|---|
align | end center start justify | Sets the text-align property | |
backgroundColor | string | Sets the background color of the cell using Workbench color token syntax, e.g. salt.400 | |
children Required | ReactNode | The content of the <th> cell | |
color | string | Sets the text color of the cell using Workbench color token syntax, e.g. salt.800 | |
valign | baseline bottom top middle | Sets the vertical-align property | |
width | string number | Establishes the width of the cell. Use percentages or fractions to ensure responsive behavior, e.g.: width={1 / 4} . |
Name | Type | Default | Description |
---|---|---|---|
align | end center start justify | Sets the text-align property | |
backgroundColor | string | Sets the background color of the cell using Workbench color token syntax, e.g. salt.400 | |
children Required | ReactNode | The content of the <th> cell | |
color | string | Sets the text color of the cell using Workbench color token syntax, e.g. salt.800 | |
valign | baseline bottom top middle | Sets the vertical-align property | |
width | string number | Establishes the width of the cell. Use percentages or fractions to ensure responsive behavior, e.g.: width={1 / 4} . | |
scope | row rowgroup col colgroup | Tells screen readers exactly what cells the header is a header for | |
sort | none ascending descending other | none | Sets 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 |
Name | Type | Default | Description |
---|---|---|---|
align | end center start justify | Sets the text-align property | |
backgroundColor | string | Sets the background color of the cell using Workbench color token syntax, e.g. salt.400 | |
children Required | ReactNode | The content of the <td> cell | |
color | string | Sets the text color of the cell using Workbench color token syntax, e.g. salt.800 | |
valign | baseline bottom top middle | Sets the vertical-align property |
- 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.
Keys | Action |
---|---|
tab + space, tab + return | Toggle sort direction on sortable table headers |
tab, shift + tab | Navigate between pagination controls |
space or return | Trigger action on pagination control buttons |
space, ↑, ↓, return | Open/close, cycle through page results control, make selection |
The following testing snippet(s) offer suggestions for testing the component using React Testing Library with occasional help from Jest.
Heads up!
Testing Table (and DataGrid) can be resource intensive and could potentially cause timeouts and flakes due to the number of DOM nodes generated. Consider breaking tests into smaller chunks to make testing more reliable.
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><TablePaginationaria-label="Billable hours table"pageSize={10}previousDisablednextDisabledonPageChange={jest.fn()}onPageSizeChange={jest.fn()}/></>,);// TableSectionconst section = screen.getByRole('region', {name: 'Billable hours',});// TableSection -> Tableconst table = within(section).getByRole('table', {name: 'Billable hours',});// TableSection -> Table -> TableRowconst [headerRow, hannahRow, isaiahRow, footerRow] = within(table).getAllByRole('row');// TableSection -> Table -> TableRow -> TableHeaderCellconst [fullNameHeaderCell, billableHoursHeaderCell] = within(headerRow).getAllByRole('columnheader',);expect(fullNameHeaderCell).toHaveAccessibleName('Full name');expect(billableHoursHeaderCell).toHaveAccessibleName('Hours');expect(billableHoursHeaderCell).toHaveAttribute('aria-sort', 'descending');// TableSection -> Table -> TableRow -> TableHeaderCellconst hannahFullNameCell = within(hannahRow).getByRole('rowheader', {name: 'Hannah Ardent',});// TableSection -> Table -> TableRow -> TableHeaderCellconst isaiahFullNameCell = within(isaiahRow).getByRole('rowheader', {name: 'Isaiah Berlin',});expect(hannahFullNameCell).toBeInTheDocument();expect(isaiahFullNameCell).toBeInTheDocument();// TableSection -> Table -> TableRow -> TableDataCellconst [hannahHoursCell] = within(hannahRow).getAllByRole('cell');expect(hannahHoursCell).toHaveAccessibleName('40');// TableSection -> Table -> TableRow -> TableDataCellconst [isaiahHoursCell] = within(isaiahRow).getAllByRole('cell');expect(isaiahHoursCell).toHaveAccessibleName('32');// TableSection -> Table -> TableRow -> TableHeaderCellconst totalBillableHoursHeaderCell = within(footerRow).getByRole('rowheader', {name: 'Total billable hours',});expect(totalBillableHoursHeaderCell).toBeInTheDocument();// TableSection -> Table -> TableRow -> TableDataCellconst [totalBillableHoursCell] = within(footerRow).getAllByRole('cell');expect(totalBillableHoursCell).toHaveAccessibleName('72');// TablePaginationconst tablePagination = screen.getByRole('navigation', {name: 'Billable hours table',});// TablePagination -> Selectconst pageSizeSelect = within(tablePagination).getByRole('combobox', {name: 'Per page:',});expect(pageSizeSelect).toHaveDisplayValue('10');// TablePagination -> Buttonconst 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();