Gesso
Data display

Table

A presentational table with sortable heads, aligned number columns, a caption, and a footer.

Table is the semantic <table> markup in a set of parts: header, body, footer, row, head, cell, and caption. It holds no data and no state, so sorting, paging, and loading are yours. A head can show a sort control and report which way it points, and a column of numbers can line up on the right. For editing a value inside a cell, Cell fields drops into any TableCell, and Pagination is driven by the host in the same way sorting is.

Owner
Open
Gesso UI kitJoe824
Washi database GUIJoe137
Subscription auditWASH-E2112
Admin table migrationMarco50

Usage

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';

<Table>
  <TableHeader>
    <TableRow>
      <TableHead sortable sortDirection="asc" onSort={sortByName}>
        Name
      </TableHead>
      <TableHead>Owner</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell>Gesso UI kit</TableCell>
      <TableCell>Joe</TableCell>
    </TableRow>
  </TableBody>
</Table>

Table wraps the <table> in a div with overflow-auto, so a table wider than its container scrolls sideways instead of pushing the page. Heads are 48px tall and cells have 16px of padding, the table sets its text at 14px in font-sans, and a row shows bg-fill on hover, and rows are ruled with the strong border. Set data-state="selected" on a TableRow to draw it in bg-selected, the brand wash a selected list or nav row uses.

Sorting

A column is sortable when its head gets sortable. The head then renders a real button over its whole width, in the tab order and pressed by Enter and Space, with a neutral caret at the right edge, so a column that can sort looks different from one that cannot before anyone clicks. sortDirection is the current direction, "asc" or "desc", and you omit it while the column is not the active sort. A head given a sortDirection shows the control even without sortable.

The button is named Sort by {label}, taken from the head's text when that is a plain string and from sortLabel otherwise. With neither, the name is just Sort. Keyboard focus draws one inset ring on the <th>, at the cell boundary, rather than a second ring on the button inside it.

<TableHead
  sortable
  sortDirection={sort?.key === 'name' ? sort.direction : undefined}
  onSort={() => toggle('name')}
>
  Project
</TableHead>

The comparison is yours. The head reports which way the arrow points and nothing else, so sorting on the server, with a cursor, or with TanStack Table's own sort state all drive it the same way. Click Project or Effort in the table above to sort it.

Alignment

TableHead and TableCell take align, one of start, center, or end. Set it on the head and on every cell in the column, since the table has no column object to carry it. Numbers read best with end and tabular-nums. A sortable head aligned to the end puts its caret before the label, so the label still lines up with the values below it.

<TableHead sortable align="end" onSort={() => toggle('sent')}>
  Sent
</TableHead>

<TableCell align="end" className="tabular-nums">
  {row.sent}
</TableCell>

Use align rather than text-right on a head. Its label sits in a flex row, so a text class alone does not move it. start and end map to the physical left and right, so they do not flip in a right-to-left layout.

Grouped rows

For a total with the parts behind it, such as an outcome and its statuses, render the parts as rows under the group and let them read as one block. There is no group variant, only four classes:

  • The group row and every part but the last drop their rule with border-b-0.
  • The group's cells hand most of their bottom padding to the block with pb-1.5.
  • Part cells take pt-1.5 pb-1.5, and the last part's take pb-4, so the block ends on the table's usual spacing.
  • The part's first cell indents with pl-8 and its text goes muted, so the group label leads.
Outcome
Recipients
Share
Delivered1,14895.3%
Bounced413.4%
Hard bounce292.4%
Soft bounce121.0%
Not sent151.2%
Suppressed110.9%
Invalid address40.3%
<TableRow className="border-b-0">
  <TableCell className="pb-1.5 font-medium">Bounced</TableCell>
  <TableCell align="end" className="pb-1.5 font-medium tabular-nums">41</TableCell>
</TableRow>
<TableRow className="border-b-0">
  <TableCell className="pl-8 pt-1.5 pb-1.5 text-fg-muted">Hard bounce</TableCell>
  <TableCell align="end" className="pt-1.5 pb-1.5 text-fg-muted tabular-nums">29</TableCell>
</TableRow>
<TableRow>
  <TableCell className="pl-8 pt-1.5 pb-4 text-fg-muted">Soft bounce</TableCell>
  <TableCell align="end" className="pt-1.5 pb-4 text-fg-muted tabular-nums">12</TableCell>
</TableRow>

A group with no parts stays an ordinary row. Name each part so it makes sense on its own, since a screen reader reads the rows in order without the indent.

TableCaption names the table and sits below it, in text-fg-muted at 14px. TableFooter is a <tfoot> on bg-surface-sunken in medium weight, with a rule above it, for a total or a summary row.

Open issues by project
Project
Open
Gesso UI kit24
Washi database GUI7
Subscription audit112
Admin table migration0
Total143
<Table>
  <TableCaption>Open issues by project</TableCaption>
  {/* header and body */}
  <TableFooter>
    <TableRow>
      <TableCell>Total</TableCell>
      <TableCell align="end" className="tabular-nums">143</TableCell>
    </TableRow>
  </TableFooter>
</Table>

Props

Every part takes the props of the element it renders. Table, TableHead, and TableCell take these as well.

PropTypeDefaultWhat it does
scrollLabelstringThe table's aria-label, or "Scrollable table"On Table. Names the scroll region a wide table becomes, so a keyboard user can focus and scroll it.
sortablebooleanfalseOn TableHead. Renders the sort button and a neutral caret, and sets aria-sort.
sortDirection"asc" | "desc"On TableHead. The current direction, which swaps the caret for an ascending or descending glyph. Omit it while the column is not the active sort.
onSort() => voidOn TableHead. Called when the sort button is pressed by click, Enter, or Space.
sortLabelstringThe head's text, when it is a stringOn TableHead. The name in Sort by {sortLabel}, for a head whose children are not a plain string.
align"start" | "center" | "end""start"On TableHead and TableCell. Aligns the text and, on a sortable head, the button.

TableAlign is exported for typing a column definition.

Accessibility

  • Table renders a native <table> with <thead>, <tbody>, <tfoot>, <tr>, <th>, <td>, and <caption>, so assistive tech announces the row and column of each cell and reads a caption as the table's name.
  • TableHead renders a <th scope="col">, and a scope you pass replaces it. TableCell always renders a <td>, so a row head needs your own <th scope="row">.
  • A sortable head sets aria-sort on the <th>, to none while it is unsorted and to ascending or descending while it is the active sort. A head that cannot sort sets none.
  • The sort control is a native <button type="button"> named Sort by {label}, so Enter and Space press it. A change of sort is announced through a polite live region, like "Sorted by Name, ascending", and the sort the table loads with is not.
  • Focus on a sort button draws a 2px inset ring on the head, in the ring color.
  • On a touch screen the sort button is at least 44px tall.
  • When the table is wider or taller than its container, the scrolling wrapper becomes a region named by scrollLabel and takes focus, with a 2px inset ring, so the arrow keys scroll it. A table that fits adds no tab stop.
  • Row selection is a style only. data-state="selected" draws the look, and exposing the selection (aria-selected in a grid, or a checked checkbox in the row) is the host's job.
  • Nothing is sticky, so a long table scrolls its head out of view.
  • The caret's color change stops under reduced motion.

When to use something else

  • Cell fields when cells need to be edited. It renders and edits a typed value inside a TableCell from this page.
  • Record list for records grouped into buckets such as status, one line each, with the keyboard moving between them and Enter opening one. It has no columns to sort.
  • Description list for the facts about one thing, as labels and values, rather than many things in columns.
  • A plain list, such as Item rows, for a short run of rows that have no columns to compare.
  • A data grid library when the table needs virtualized rows, resizable, reorderable, or pinned columns, row selection or grouping state, or arrow-key movement between cells. Table is markup and takes whatever such a library computes, and TanStack Table's headless state can drive it, but Table adds none of that itself.

Install

shadcn add @gesso/table

On this page