Table

Rows and columns for records that share the same fields, with sorting, selection and an empty state you control.

import { Badge } from "@hel/ui/badge"
import type { BadgeProps } from "@hel/ui/badge"
import { DateText } from "@hel/ui/date-text"

When to use it

Use a table when people compare many records by the same fields: orders, customers, products, invoices. The columns say what each value means, and the rows can be scanned, sorted and selected.

Do not use a table for layout, or for the details of one record. For one record, use a Description List. For a few figures, use Cards.

This is the presentational layer. It draws the rows and keeps the header in view, but it owns no data. Sorting, selecting and filtering are state you hold yourself, as the examples below do, or the state of a table library that you connect to the parts.

Import

import {
  Table,
  TableCaption,
  TableHeader,
  TableBody,
  TableFooter,
  TableRow,
  TableHead,
  TableCell,
} from "@hel/ui/table"

Anatomy

<Table>
  <TableCaption>Recent orders</TableCaption>
  <TableHeader>
    <TableRow>
      <TableHead>Order</TableHead>
      <TableHead align="end">Total</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell>#10482</TableCell>
      <TableCell align="end" numeric>$1,840.00</TableCell>
    </TableRow>
  </TableBody>
</Table>

These are the native table elements, so screen readers read them as a table. The TableCaption names the table and is read by screen readers only, unless you set visible.

A table that is wider than its space scrolls sideways inside its own frame, and the scrollbar shows on hover. Size the frame with containerClassName, and use className for the <table> itself.

Right-align number columns with align="end" on both the head and the cells, and add numeric to the cells, so the digits line up. Use tone="muted" on cells that hold secondary data, and truncate to cut off a long value with an ellipsis, which works with layout="fixed" or a set column width.

Examples

Sorting

A header cell shows a sort state with sort (asc, desc or none) and reports a press with onSort. It does not sort anything itself, so you decide what pressing does: here it sorts by the column, and flips the direction when pressed again. Setting sort also sets aria-sort for screen readers.

import * as React from "react"

import { Money } from "@hel/ui/money"

A header can instead open a menu, with menu and items such as sort, hide and pin. It takes the place of onSort.

Selecting rows

Put a checkbox in a TableSelectHead and in each TableSelectCell. The row takes the raised surface when you set selected on it. The checkbox in the header can show the in-between state with indeterminate, when some rows are selected. Label each checkbox, because it has no visible text.

1 of 4 selected

import * as React from "react"

import { Button } from "@hel/ui/button"

Nothing to show

TableEmptyRow is one row that spans the table, for a search with no results or a list that has no records yet. Give it the number of columns in colSpan, and put an Empty inside, so people can see why, and what to do next.

import * as React from "react"
import { SearchX } from "lucide-react"

Density and surface

density is compact (32px rows) or comfortable (40px). surface is the color behind the table, and it matters when columns are pinned, because a pinned cell paints it to hide what scrolls under it. Use surface="raised" for a table inside a Card.

fill makes the table take the height of a parent with a fixed height, and scroll inside it under a sticky header. The header is sticky by default, and sticky={false} on TableHeader turns it off.

Props

Table

PropTypeDefault
density"compact" | "comfortable""compact"
layout"auto" | "fixed""auto"
surface"page" | "raised" | "muted""page"
fillbooleanfalse
containerClassNamestringnone

TableHead

PropTypeDefault
sort"asc" | "desc" | "none"none
onSort() => voidnone
menuReactNodenone
iconLucideIconnone
align"start" | "center" | "end""start"
trailingReactNodenone

TableRow

PropTypeDefault
selectedbooleanfalse
hoverablebooleantrue
draggingbooleanfalse

TableCell

PropTypeDefault
align"start" | "center" | "end""start"
tone"primary" | "muted""primary"
numericbooleanfalse
truncatebooleanfalse

Other parts

TableHeader takes sticky and spaced. TableCaption takes visible. TableEmptyRow takes colSpan. For a table with pinned columns, resizable columns or rows that can be dragged, there are pinned and pinEdge on the heads and cells, TableColumnResizer, TableHandleHead, TableHandleCell and TableDragHandle. They only draw the parts, and the app supplies the behavior.