Table

A real table, styled and made to scroll on narrow screens. Compound down to the cell, so the consumer decides what goes in each one.

API

Props

proptypedefault
as?ElementType"table"the element or component to render
className?stringmerged over the defaults, so it wins
…restprops of astyped against the chosen element

Table.Head

proptypedefault
as?ElementType"thead"the element or component to render
className?stringmerged over the defaults, so it wins
…restprops of astyped against the chosen element

Table.Body

proptypedefault
as?ElementType"tbody"the element or component to render
className?stringmerged over the defaults, so it wins
…restprops of astyped against the chosen element

Table.Row

proptypedefault
as?ElementType"tr"the element or component to render
className?stringmerged over the defaults, so it wins
…restprops of astyped against the chosen element

Table.HeadCell

proptypedefault
as?ElementType"th"the element or component to render
className?stringmerged over the defaults, so it wins
…restprops of astyped against the chosen element

Table.Cell

proptypedefault
as?ElementType"td"the element or component to render
className?stringmerged over the defaults, so it wins
…restprops of astyped against the chosen element

Examples

Usage

tsx
<Table>
  <Table.Head>
    <Table.Row>
      <Table.HeadCell>prop</Table.HeadCell>
      <Table.HeadCell>type</Table.HeadCell>
      <Table.HeadCell>default</Table.HeadCell>
    </Table.Row>
  </Table.Head>
  <Table.Body>
    <Table.Row>
      <Table.Cell>active</Table.Cell>
      <Table.Cell>boolean</Table.Cell>
      <Table.Cell>false</Table.Cell>
    </Table.Row>
    <Table.Row>
      <Table.Cell>as</Table.Cell>
      <Table.Cell>ElementType</Table.Cell>
      <Table.Cell>"div"</Table.Cell>
    </Table.Row>
  </Table.Body>
</Table>
proptypedefault
activebooleanfalse
asElementType"div"

Details

Compound down to the cell

The kit could have taken columns and rows as data and rendered the markup for you. It does not, because the moment one cell needs a chip, a link or a second line, a data-driven table either grows a render prop per column or stops being usable. Here the cells are yours from the start.

Mapping is therefore explicit, and pairs naturally with List — an empty array renders an empty body rather than a broken table.

tsx
<Table.Body>
  <List
    array={rows}
    itemExtractor={({ row }) => (
      <Table.Row key={row.id}>
        <Table.Cell>{row.name}</Table.Cell>
      </Table.Row>
    )}
  />
</Table.Body>

Cells do not wrap

Cells are whitespace-nowrap by default. A table that wraps its cells turns into a block of ragged text at the first narrow column, and the row loses the alignment that made it a table. When a column is genuinely prose — a description, a message — opt that one back into wrapping.

tsx
<Table.Cell className="whitespace-normal">{row.description}</Table.Cell>

It stays a table

Each part renders its real element — table, thead, tbody, tr, th, td — rather than divs with a grid. Screen readers announce row and column positions, the browser handles headers, and none of that has to be reimplemented with aria attributes.