Navigation

v0.0.4
Data display

Table

Rows of data on hairlines, with sortable headings and aligned figures.

On this page

A table sets rows of data on hairlines. Column headings are quiet captions on a firmer rule, and the rows carry the ink. Put table on the <table>, table-row on each <tr>, table-heading on column headings and table-cell on data cells and row headings. The table has no surface of its own, so put it in a sheet or a card.

Table

<div class="sheet overflow-hidden">  <table class="table">    <thead>      <tr class="table-row">        <th class="table-heading" scope="col">Project</th>        <th class="table-heading" scope="col">Status</th>        <th class="table-heading" scope="col">Region</th>        <th class="table-heading table-numeric" scope="col">Requests</th>      </tr>    </thead>    <tbody>      <tr class="table-row">        <th class="table-cell table-primary" scope="row">Atlas</th>        <td class="table-cell">          <span class="badge badge-success badge-dot">Ready</span>        </td>        <td class="table-cell">Tokyo</td>        <td class="table-cell table-numeric">1,284,019</td>      </tr>      <tr class="table-row">        <th class="table-cell table-primary" scope="row">Borealis</th>        <td class="table-cell">          <span class="badge badge-warning badge-dot">Degraded</span>        </td>        <td class="table-cell">Frankfurt</td>        <td class="table-cell table-numeric">94,201</td>      </tr>      <tr class="table-row">        <th class="table-cell table-primary" scope="row">Cirrus</th>        <td class="table-cell">          <span class="badge badge-dot">Draft</span>        </td>        <td class="table-cell">Virginia</td>        <td class="table-cell table-numeric">0</td>      </tr>    </tbody>  </table></div>

#Cells

Use <th class="table-cell" scope="row"> when a cell names its row, or scope="rowgroup" when it names a group. It aligns to the start and uses --font-weight-control, as does a table-cell with role="rowheader" in a grid of roles. Ordinary cells keep the surrounding weight; the browser's centered, bold <th> default does not change the table's rhythm.

The same classes supply table, row and cell display for a table of ARIA roles, including with standalone CSS.

  • table-primary marks the cell that identifies the row and gives it strong ink. The other cells stay in body ink, so the eye finds the name first.
  • table-numeric on a heading and its cells sets them on the end edge in tabular figures. Digits then stack in columns and amounts can be compared by length.

#Sorting

A heading with aria-sort="ascending" or aria-sort="descending" takes strong ink and a chevron that points the way the values run. Put a button inside the heading to change the order; the stylesheet reads the attribute, and your script or your server sets it.

Sorting

<div class="sheet overflow-hidden">  <table class="table table-compact">    <thead>      <tr class="table-row">        <th class="table-heading" scope="col" aria-sort="ascending">Name</th>        <th class="table-heading" scope="col">Role</th>        <th class="table-heading table-numeric" scope="col">Deployments</th>      </tr>    </thead>    <tbody>      <tr class="table-row">        <td class="table-cell table-primary">Ada Lovelace</td>        <td class="table-cell">Owner</td>        <td class="table-cell table-numeric">412</td>      </tr>      <tr class="table-row">        <td class="table-cell table-primary">Alan Turing</td>        <td class="table-cell">Admin</td>        <td class="table-cell table-numeric">96</td>      </tr>      <tr class="table-row">        <td class="table-cell table-primary">Grace Hopper</td>        <td class="table-cell">Member</td>        <td class="table-cell table-numeric">1,204</td>      </tr>    </tbody>  </table></div>

table-compact tightens the rows, for long lists that are read by scanning.

#Selection and rows that open

A row with aria-selected="true" takes a chip tone. table-interactive on the table makes every body row answer the pointer at once, the way a row in a menu does. The row paints these tones, not its cells, so no seam shows where two cells meet.

Selection

<div class="sheet overflow-hidden">  <table class="table table-interactive">    <thead>      <tr class="table-row">        <th class="table-heading" scope="col">          <input type="checkbox" class="checkbox" aria-label="Select all">        </th>        <th class="table-heading" scope="col">Domain</th>        <th class="table-heading" scope="col">Certificate</th>      </tr>    </thead>    <tbody>      <tr class="table-row" aria-selected="true">        <td class="table-cell">          <input type="checkbox" class="checkbox" checked aria-label="Select atlas.dev">        </td>        <td class="table-cell table-primary">atlas.dev</td>        <td class="table-cell">Renews in 61 days</td>      </tr>      <tr class="table-row">        <td class="table-cell">          <input type="checkbox" class="checkbox" aria-label="Select docs.atlas.dev">        </td>        <td class="table-cell table-primary">docs.atlas.dev</td>        <td class="table-cell">Renews in 12 days</td>      </tr>    </tbody>  </table></div>

#Wide tables

A table keeps its columns: it does not restack into cards, because a restacked table loses the comparison a table exists for. When it can be wider than its column, wrap it in a scroll area that scrolls along the inline axis; the edge fades where columns continue. table-sticky keeps the headings in view while rows scroll under them.

Wide tables

656 px
<div class="sheet overflow-hidden">  <div class="scroll-area scroll-area-inline" tabindex="0" role="region" aria-label="Deployments">    <table class="table">      <thead>        <tr class="table-row">          <th class="table-heading" scope="col">Deployment</th>          <th class="table-heading" scope="col">Branch</th>          <th class="table-heading" scope="col">Commit</th>          <th class="table-heading" scope="col">Author</th>          <th class="table-heading table-numeric" scope="col">Duration</th>        </tr>      </thead>      <tbody>        <tr class="table-row">          <td class="table-cell table-primary whitespace-nowrap">atlas-9f2c1a</td>          <td class="table-cell whitespace-nowrap">main</td>          <td class="table-cell whitespace-nowrap">Soften every edge</td>          <td class="table-cell whitespace-nowrap">Ada Lovelace</td>          <td class="table-cell table-numeric">48s</td>        </tr>        <tr class="table-row">          <td class="table-cell table-primary whitespace-nowrap">atlas-71be0d</td>          <td class="table-cell whitespace-nowrap">feature/anchors</td>          <td class="table-cell whitespace-nowrap">Glide the segmented thumb</td>          <td class="table-cell whitespace-nowrap">Alan Turing</td>          <td class="table-cell table-numeric">1m 02s</td>        </tr>      </tbody>    </table>  </div></div>
Narrow the preview: the table scrolls inside its sheet and its end edge fades.

#Accessibility

  • Use a real <table> with <thead>, <tbody> and scope="col" on headings, so each cell is announced with its column.
  • A scrolling wrapper needs tabindex="0", role="region" and a label, so the keys can scroll it.
  • Selection is said twice on purpose: aria-selected on the row and the state of its checkbox.

#Classes

ClassDescription
tableThe table: a native table or a grid of roles
table-rowA row
table-headingA column heading: a quiet caption on a firmer rule than the rows
table-cellA start-aligned data cell or row header. A native scope=row/rowgroup or role=rowheader uses the theme’s control weight instead of browser bold
table-primaryThe identifying cell of a row, in strong ink. With table-cell
table-numericRight-aligned tabular figures, on a heading or a cell. With table-heading or table-cell
table-compactDenser rows, for long lists read by scanning. With table
table-interactiveRows that hold cells answer the pointer and show a focus ring. With table
table-stickyHeadings stay in view while the rows scroll under them. With table

#Custom properties

PropertyDefaultDescription
--table-padding-x1remInline padding of headings and cells; .75rem in a compact table
--table-padding-y.75remBlock padding of cells; .4375rem in a compact table

#Source

@master/ui/components/table.css
/* * Table: rows of data on hairlines. Every part is reached by class, so the same styles serve a native <table> * and a grid of roles. Headings are quiet captions; a sortable heading shows its direction from aria-sort; * numbers line up on tabular figures. Put the table in a sheet for a surface and in a scroll area when it can * be wider than its column. */@layer components {  .table {    --table-padding-x: 1rem;    --table-padding-y: .75rem;    display: table;    width: 100%;    border-spacing: 0;    border-collapse: separate;    color: var(--color-text-body);    font-size: .875rem;    line-height: 1.45;    text-align: start;    /* Denser rows, for long lists read by scanning. */    &.table-compact {      --table-padding-x: .75rem;      --table-padding-y: .4375rem;      font-size: .8125rem;    }  }  /* A column heading: a quiet caption on a firmer rule than the rows below it. */  .table-heading {    display: table-cell;    padding: .625rem var(--table-padding-x);    border-bottom: 1px solid var(--color-line-divider);    color: var(--color-text-muted);    font-size: .75rem;    font-weight: var(--font-weight-control);    text-align: start;    white-space: nowrap;    vertical-align: bottom;    /* Sorted: the heading takes strong ink and a chevron that points the way the values run. */    &:is([aria-sort="ascending"], [aria-sort="descending"]) {      color: var(--color-text-strong);      &::after {        content: '';        display: inline-block;        width: .75rem;        height: .75rem;        margin-inline-start: .25rem;        background: currentColor;        mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4.5 6.5 3.5 3.5 3.5-3.5'/%3E%3C/svg%3E") center / contain no-repeat;        vertical-align: -.125em;      }    }    &[aria-sort="ascending"]::after {      rotate: x 180deg;    }    @media (forced-colors: active) {      border-bottom-color: CanvasText;      &::after {        background: CanvasText;      }    }  }  .table-cell {    display: table-cell;    padding: var(--table-padding-y) var(--table-padding-x);    border-bottom: 1px solid var(--color-line-hair);    vertical-align: middle;    font-variant-numeric: tabular-nums;    font-weight: inherit;    text-align: start;    /* Native row headers must not fall back to the browser's centered, bold <th>. */    &:is([scope="row"], [scope="rowgroup"], [role="rowheader"]) {      font-weight: var(--font-weight-control);    }    @media (forced-colors: active) {      border-bottom-color: GrayText;    }  }  /* Numbers and amounts: on the end edge, in figures of one width, so digits stack in columns. */  .table-heading.table-numeric, .table-cell.table-numeric {    font-variant-numeric: tabular-nums;    text-align: end;  }  /* The identifying cell of a row, in strong ink. */  .table-cell.table-primary {    color: var(--color-text-strong);  }  .table-row {    display: table-row;    /* The last row ends the table; the surface around it draws the edge. */    &:last-child .table-cell {      border-bottom-color: transparent;    }    /*     * A selected row takes a chip tone. The row paints it, not its cells, so no seam shows where two cells     * meet. Say it with aria-selected as well as with its checkbox.     */    &[aria-selected="true"] {      background: var(--color-surface-chip);    }  }  /* Rows that can be opened or selected answer the pointer at once, like rows in a menu. */  .table-interactive .table-row {    @media (hover: hover) {      &:hover:has(.table-cell) {        background: var(--color-surface-chip);      }    }    /* Pressed: a step deeper than the pointer's tone. */    &:active:has(.table-cell) {      background: var(--color-surface-inset-hover);    }    &:focus-visible {      outline: 2px solid var(--color-focus);      outline-offset: -2px;    }  }  /* Headings stay in view while the rows scroll under them, on the color of the surface they sit on. */  .table-sticky .table-heading {    position: sticky;    top: 0;    z-index: 1;    background: var(--surface-color, var(--color-surface-base));  }}