Skip to content
txbrendan/ui

Data Table

beta

A dense table on Table: sortable columns, people through Person, statuses, peeks, your row, zones, breaks, and quick actions per row.

Preview

Person, sortable, status, peek, rowActions, highlight, onRowClick

Team
RoleFees
Tom HaleDesigner42Paid
Josh TaitNo replyEngineer17
You, Mia OkaforWriter31Paid
Cal NashDeclinedAnalyst8Paid
Ana RuizSupport25

Press a row

zones, breaks, highlight in a zone

Most tickets closed
#NameClosed
1Mia Okafor9
2Cal Nash7
Top two
3Tom Hale5
4Ana Ruiz4
Your place
5You, Josh Tait2

loading, empty

NameWhen
NameWhen
No one has signed up yet

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/data-table

Use it for

  • Rows of records read across: a team, payments, attendance. Put it in a Card with className="px-0" on the part that holds it, so it runs edge to edge.
  • It is generic over the row: every callback sees your row type. Keep columns stable, outside the component or in useMemo, so a sort is not redone each render.
  • A field shaped { person, photo } draws as Person. Use render for anything else.
  • status puts the row's status right after a cell's value, usually the name. A table's status follows the name, never its own column.
  • peek explains a value: who, when, why. Plain text gets a dotted underline.
  • sortable adds a header button. The first press sorts text A to Z and numbers high to low, and the next press flips it. aria-sort says which.
  • highlight marks your rows with a You tag and their own wash. zones tint a run of rows, like the top places, with a 2px rule where it meets the rest. breaks add a labeled line, like Your place.
  • rowActions spring in over the row's end on hover or focus, and their keys work then. If they would cover a peek or a status, the table keeps a band at the end for them.
  • hideBelow drops a column while the table is narrow, so the names keep their room.

Avoid it for

  • A list of things that are not read across. Use ListRow.
  • Sorting a table with zones or breaks. They follow row positions, so a sort moves them onto other rows.
  • A column for the status. Put status on the name column.
  • Colors alone for a zone. Say what it means in a peek or in words.

Props

  • columns

    Required. Each: { key, label, short?, width?, align?, numeric?, render?(row, hovered), peek?(row), underline?, status?(row), rowHeader?, sortable?, sortValue?(row), hideBelow?, className? }. short shows in place of label, like "P" for Played, while label is read.

    DataColumn<Row>[]

  • rows

    Required. Long tables draw 100 rows at a time as the reader nears the end.

    Row[]

  • rowKey

    A stable key per row. Default row.id, then the position.

    string | ((row, index) => string | number)

  • breaks

    A labeled line after a row index. Its arrow says whether the label is about the rows above or below.

    { after: number; label: string; tone?; side?: "above" | "below" }[]

  • zones

    Runs of row indices tinted in a tone as one band, with a 2px rule where the band meets the rest.

    { from: number; to: number; tone? }[]

  • highlight

    Rows that are yours: a You tag after the name and their own wash, or the tag alone inside a zone.

    (row) => boolean

  • highlightLabel

    The tag on a highlighted row, which screen readers hear before its name.

    string · default "You"

  • rowActions

    Quick actions over the row's end while it is hovered or holds focus.

    (row) => HoverAction[]

  • onRowClick

    Makes each row one press, named by its row header.

    (row) => void

  • sort

    Controlled sort.

    { key: string; direction: "ascending" | "descending" } | null

  • defaultSort

    The first sort, uncontrolled.

    { key: string; direction: "ascending" | "descending" } | null

  • onSortChange

    Hears each sort a header button asks for.

    (sort) => void

  • caption

    Names the table, under it.

    ReactNode

  • aria-label

    Names the table when it has no caption.

    string

  • loading

    Holds the space with three placeholder rows.

    boolean · default false

  • empty

    What shows with no rows.

    ReactNode · default "Nothing here yet"

  • data-slot

    The root's slot, for a part built on DataTable, like Leaderboard.

    string · default "data-table"

  • className

    Extra classes on the wrapper. Set --data-table-edge to change the first and last columns' inset. Later classes win.

    string

Accessibility

  • A real <table>, with column headers. A column marked rowHeader is a <th scope="row">, so each cell is read with the row's name.
  • A sortable header is a button inside the <th>. The sorted header carries aria-sort, ascending or descending.
  • With onRowClick, each row's press is one real button named by its row header. Its focus ring goes around the whole row. Controls in the row keep their own presses, and none sits inside another.
  • A highlighted row's row header starts with "You," for screen readers, so every cell is read with it. Its tag is words, so it stays in forced colors.
  • A zone's edges are 2px borders, so they stay in forced colors. Its meaning needs words, like a peek on its rows.
  • Row actions show on hover and on focus inside the row. Their single-key shortcuts work only while the row is lit.
  • Each row carries data-peek-row, so a peek opens beside it, and ↑↓ step between peeks.
  • While loading, the table is aria-busy.

Examples

Sortable columns with people

type Member = { id: string; name: { person: string }; role: string; projects: number };

const columns: DataColumn<Member>[] = [
  { key: "name", label: "Name", rowHeader: true, sortable: true, sortValue: (row) => row.name.person },
  { key: "role", label: "Role" },
  { key: "projects", label: "Projects", numeric: true, sortable: true },
];

<DataTable
  aria-label="Team"
  columns={columns}
  rows={[
    { id: "p1", name: { person: "Tom Hale" }, role: "Designer", projects: 42 },
    { id: "p2", name: { person: "Josh Tait" }, role: "Engineer", projects: 17 },
  ]}
/>