Cmpnts

Menu

Organisms

Table

Data in rows and columns, with rules, alignment and sticky edges.


Ferns in stock
Plant Height (cm) In stock
Hart's tongue 40 18
Lady fern 60 7
Code
HTML
<div class="table-container" tabindex="0" role="region" aria-labelledby="default-stock">
  <table class="table">
    <caption id="default-stock" class="small-heading">Ferns in stock</caption>
    <thead>
      <tr>
        <th scope="col">Plant</th>
        <th scope="col" class="table__cell--numeric">Height (cm)</th>
        <th scope="col" class="table__cell--numeric">In stock</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">Hart's tongue</th>
        <td class="table__cell--numeric">40</td>
        <td class="table__cell--numeric">18</td>
      </tr>
      <tr>
        <th scope="row">Lady fern</th>
        <td class="table__cell--numeric">60</td>
        <td class="table__cell--numeric">7</td>
      </tr>
    </tbody>
  </table>
</div>

Why it exists

A Table is for comparing, across a row or down a column. Only a real <table> tells a screen reader which headers belong to a cell, so someone moving through it hears the column and the row as they go. That is why it uses <th> with scope, and why nothing here changes the display of its elements.

A wide Table scrolls in its container, and the library won’t reflow rows into Cards. Stacked Cards keep each value but lose the columns, and the columns are the reason to use a Table at all.

Sorting, selection and paging depend on your data, so they are the page’s job. The Table only styles their states.

When to use

  • For data with rows and columns, where people compare across them.
  • Not for layout: a layout grid is not a Table; use a grid.
  • Not for name and value pairs on their own: Telemetry is a description list.
  • Not for a list of things with one main link: use a Card or a list.

Rules

  • Don’t put display on table elements: it drops their Table semantics in some browsers.
  • Wide Tables scroll in their container; the library doesn’t reflow rows into Cards.
  • Scroll container: a Table that can be wider than its column sits in .table-container, with tabindex="0", role="region" and a unique name (aria-labelledby the caption, or aria-label), so it can be scrolled by keyboard and found (WCAG 2.1.1, 1.4.10).
  • Caption: names the Table. It sets no type: put a text atom on it (.small-heading), or .sr-only to hide it.
  • Headers: scope="col" on column headers, scope="row" on the first cell of a row that names it.

Keyboard and screen readers

The browser provides all of this, given a real <table> and a focusable container. The page’s script only sorts the rows and updates the state.

  • Tab reaches the .table-container, then each control inside it, in reading order.
  • Arrow keys scroll the container once it has focus.
  • Enter and Space press a sort Button.
  • A screen reader finds the container as a region by its name, moves from cell to cell and reads the headers from scope. It takes the sort order from aria-sort on the <th>.

Let a Wide Table Scroll

Plants in stock
Plant Height (cm) In stock
Epimedium 30 42
Total 42
Code
HTML
<div class="table-container" tabindex="0" role="region" aria-labelledby="stock">
  <table class="table">
    <caption id="stock" class="small-heading">Plants in stock</caption>
    <thead>
      <tr>
        <th scope="col">Plant</th>
        <th scope="col" class="table__cell--numeric">Height (cm)</th>
        <th scope="col" class="table__cell--numeric">In stock</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">Epimedium</th>
        <td class="table__cell--numeric">30</td>
        <td class="table__cell--numeric">42</td>
      </tr>
    </tbody>
    <tfoot>
      <tr>
        <th scope="row" colspan="2">Total</th>
        <td class="table__cell--numeric">42</td>
      </tr>
    </tfoot>
  </table>
</div>

Sorting

Plants by height
Epimedium30
Brunnera45
Code
HTML
<div class="table-container" tabindex="0" role="region" aria-labelledby="stock-sorted">
  <table class="table">
    <caption id="stock-sorted" class="small-heading">Plants by height</caption>
    <thead>
      <tr>
        <th scope="col">
          <button type="button" class="table__sort-button">
            Plant
            <svg class="icon" width="1em" height="1em" aria-hidden="true" focusable="false"><use href="#ChevronsUpDown"/></svg>
          </button>
        </th>
        <th scope="col" class="table__cell--numeric" aria-sort="ascending">
          <button type="button" class="table__sort-button">
            Height (cm)
            <svg class="icon" width="1em" height="1em" aria-hidden="true" focusable="false"><use href="#ChevronDown"/></svg>
          </button>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr><th scope="row">Epimedium</th><td class="table__cell--numeric">30</td></tr>
      <tr><th scope="row">Brunnera</th><td class="table__cell--numeric">45</td></tr>
    </tbody>
  </table>
</div>
  • aria-sort goes on the <th>, on one column at a time. The Button is named by its visible text; don’t replace it with an aria-label.
  • Unsorted columns show ChevronsUpDown; the sorted column shows ChevronDown, which points up for aria-sort="ascending". The page’s script sorts the rows, sets aria-sort and swaps the icon.
  • Say the sort order in the caption or a role="status" message if it matters (“Sorted by height, ascending”).

Rows with Actions

PlantActions
Brunnera
Code
HTML
<div class="table-container" tabindex="0" role="region" aria-label="Plants to reorder">
  <table class="table table--hover">
    <thead><tr><th scope="col">Plant</th><th scope="col" class="table__col--actions"><span class="sr-only">Actions</span></th></tr></thead>
    <tbody>
      <tr>
        <th scope="row">Brunnera</th>
        <td class="table__col--actions">
          <button type="button" class="button button--subtle button--sm">Reorder<span class="sr-only"> Brunnera</span></button>
        </td>
      </tr>
    </tbody>
  </table>
</div>
  • --hover only for rows with actions or selection; a highlight on a read-only row suggests it does something. A row whose control has focus is highlighted too.
  • A control doesn’t make its row taller than the control. Name each row’s Buttons uniquely (hidden text with the row’s name).

Do and Don’t

DoDon’t
Put a wide Table in a focusable, named .table-container.Reflow rows into Cards, or leave a scroller keyboard users can’t reach.
Name the Table with a <caption> and a text atom.Leave it unnamed, or look for a caption class.
Put aria-sort on the <th>.Put it on the sort Button.
Use --hover only where rows have actions or selection.Highlight read-only rows.
End-align numbers with .table__cell--numeric.Centre a column of figures.
Use a Grid for layout.Set display on table elements.

In Storybook

See also

  • Card: A surface with named parts, such as a header, a body and a footer, for one thing a person can read, compare or act on.
  • Badge, Status, Chip and Kbd: Small labels chosen by their job, such as a count on something, the state of something, a value someone chose or a key to press.
  • Button: An action on the page, such as saving a Form, opening a Dialog or changing a setting.
  • Pagination: Links that move between the pages of one List.
  • Batch Actions: The bar shown while items are selected, with how many, the actions on them and a way to clear the selection.

Search the docs