Skip to content

Table

Rows and columns of data with a divider between rows, and a scroll area for tables wider than the page.

BandFrequencyGainQ
Low shelf120 Hz+2.5 dB0.71
Bell1.2 kHz-3.0 dB1.40
High shelf8 kHz+1.0 dB0.71
import { Table } from '@contour-ui/react';

<Table.Root>
  <Table.Header>
    <Table.Row>
      <Table.ColumnHeader>Band</Table.ColumnHeader>
      <Table.ColumnHeader>Gain</Table.ColumnHeader>
    </Table.Row>
  </Table.Header>
  <Table.Body>
    <Table.Row>
      <Table.Cell>Low shelf</Table.Cell>
      <Table.Cell>+2.5 dB</Table.Cell>
    </Table.Row>
  </Table.Body>
</Table.Root>;

The table fills the width of its container. Cells are start-aligned; to align a column, set textAlign inline on its cells, as Markdown tables do: <Table.Cell style={{ textAlign: 'end' }}>.

Variants and sizes

line, the default, draws a divider between rows; outline also draws a rounded border around the table. size sets the cell padding: md for reading, sm for dense data.

BandFrequencyGain
Low shelf120 Hz+2.5 dB
High shelf8 kHz+1.0 dB
<Table.Root variant="outline" size="sm">
  …
</Table.Root>

Table.Caption names the table and sits under it; put it first in Table.Root, as HTML requires. Table.Footer holds summary rows such as totals; a Table.ColumnHeader with scope="row" labels such a row.

CPU load per plugin
PluginCPU
EQ6.5%
Reverb9.5%
Total16.0%
<Table.Root>
  <Table.Caption>CPU load per plugin</Table.Caption>
  <Table.Header>…</Table.Header>
  <Table.Body>…</Table.Body>
  <Table.Footer>
    <Table.Row>
      <Table.ColumnHeader scope="row">Total</Table.ColumnHeader>
      <Table.Cell style={{ textAlign: 'end' }}>16.0%</Table.Cell>
    </Table.Row>
  </Table.Footer>
</Table.Root>

Wide tables

When space runs short, cells wrap their text. A table that still does not fit needs Table.ScrollArea: it scrolls the table sideways instead of letting it overflow the page. Name the area after the caption, so it is announced as a region. To keep values such as numbers with units on one line, set white-space: nowrap on the table or on their cells.

Mixer channels
ChannelInputGainPanLowMidHighOutput
KickIn 1+4.0 dBC+2.0 dB0.0 dB-1.5 dBDrums
BassDI 1+3.0 dBC+1.5 dB-2.0 dB0.0 dBMain
<Table.ScrollArea aria-labelledby="mixer-caption">
  <Table.Root size="sm" style={{ whiteSpace: 'nowrap' }}>
    <Table.Caption id="mixer-caption">Mixer channels</Table.Caption>
    <Table.Header>…</Table.Header>
    <Table.Body>…</Table.Body>
  </Table.Root>
</Table.ScrollArea>

Anatomy

PartElementClassAttributes
Table.ScrollAreadivcontour-table__scroll-areadata-scope="table" data-part="scroll-area", tabindex="0"; role="region" with aria-label or aria-labelledby
Table.Roottablecontour-tabledata-scope="table" data-part="root", data-variant, data-size
Table.Captioncaptioncontour-table__captiondata-scope="table" data-part="caption"
Table.Headertheadcontour-table__headerdata-scope="table" data-part="header"
Table.Bodytbodycontour-table__bodydata-scope="table" data-part="body"
Table.Footertfootcontour-table__footerdata-scope="table" data-part="footer"
Table.Rowtrcontour-table__rowdata-scope="table" data-part="row"
Table.ColumnHeaderthcontour-table__column-headerdata-scope="table" data-part="column-header", scope="col"
Table.Celltdcontour-table__celldata-scope="table" data-part="cell"

Props

Table.Root:

PropValuesDefault
variant'line', 'outline': dividers only, or a border around them'line'
size'sm', 'md': the cell padding'md'

Table.ColumnHeader:

PropValuesDefault
scope'col', 'row', 'colgroup', 'rowgroup''col'

Every part takes asChild and the props of its element, such as className, style and id; className is merged with the part's class.

Keyboard

KeyAction
TabMoves focus to the scroll area and shows the focus ring
← →Scroll the focused area sideways

Accessibility

  • The parts render a real <table>, so screen readers announce rows, columns and their headers. Mark every header cell with Table.ColumnHeader: it labels its column, or its row with scope="row".
  • Name the table with Table.Caption, or with aria-label on Table.Root when the name should not be shown.
  • Table.ScrollArea is in the tab order, so keyboard users can scroll it too. Name it with aria-labelledby (the caption's id) or aria-label: it becomes a region with that name, and screen readers say what the focused area holds. Each scroll area is a tab stop, so wrap only tables that can overflow.
  • Use a table for data with rows and columns, not for layout.

Tokens

TokenUsed for
--ct-text-label, --ct-color-fg-mutedcolumn headers
--ct-text-body, --ct-color-fg-defaultcells
--ct-color-border-subtle, --ct-border-width-defaultthe dividers between rows
--ct-color-border-default, --ct-radius-panelthe outline border and its corners
--ct-space-group-gap, --ct-space-panel-insetcell padding, md: block, inline
--ct-space-control-gap, --ct-space-group-gapcell padding, sm: block, inline
--ct-text-caption, --ct-color-fg-muted, --ct-space-group-gapthe caption and its gap from the table
--ct-color-border-focus, --ct-border-width-focusthe scroll area's focus ring