Skip to content
txbrendan/ui

Table

beta

A data table built from native table elements, with a caption, row sizes, and a container that scrolls sideways on narrow screens.

Preview

caption, header, body, footer

Recent invoices
InvoiceStatusMethodAmount
INV-001PaidCredit card$250.00
INV-002PendingBank transfer$150.00
INV-003OverdueCredit card$350.00
INV-004PaidPayPal$450.00
Total$1,200.00

wide, scrolls when narrow

Team members
NameEmailRoleTeamJoinedLast active
Ada Lovelaceada@example.comOwnerPlatformMar 2, 20242 minutes ago
Grace Hoppergrace@example.comAdminCompilersJan 18, 20251 hour ago
Alan Turingalan@example.comMemberResearchJul 9, 2025Yesterday

size sm

Plan limits, size sm
PlanSeats
Free3
Pro25

size md

Plan limits, size md
PlanSeats
Free3
Pro25

size lg

Plan limits, size lg
PlanSeats
Free3
Pro25

no rows

Orders
OrderCustomerTotal
No orders 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/table

Use it for

  • Showing records people compare across columns: invoices, members, orders, results.
  • Name every table with a TableCaption, as the first part inside Table.
  • Put the column names in a TableHeader row of TableHead cells. Use TableHead with scope="row" for a cell that names its row.
  • Pass scrollLabel when the table can be wider than its container, like on a phone. It makes the container a named region keyboard users can scroll.
  • Use size to set row height: sm for dense data, md by default, lg for short tables with room to breathe.
  • Right-align numbers with className="text-right" on their TableHead and TableCell, so digits line up.
  • For no rows, render one row with a single TableCell whose colSpan covers every column, and say why it is empty.

Avoid it for

  • Laying out a page or a form. Use grid or flex. A table tells screen readers the content is data.
  • One column of items. Use a list.
  • Sorting, filtering, selecting rows, or editing cells. This table only displays. Add those with real buttons and inputs in the cells.
  • Very long text in a cell. Cells do not wrap, so a paragraph makes the table very wide. Pass className="whitespace-normal" to that cell.

Props

  • size

    On Table. Row height and cell padding: a one-line row is as tall as a control of the same size. Density scales it.

    "sm" | "md" | "lg" · default "md"

  • scrollLabel

    On Table. Makes the scroll container a focusable region with this name, so keyboard users can scroll a wide table. Use the caption's words.

    string

  • Table

    A <table> inside a container that scrolls sideways. className and other props go on the <table>.

    Omit<ComponentProps<"table">, "color"> & { size?, scrollLabel? }

  • TableCaption

    Part. The table's name. Put it first inside Table. It shows below the table, left-aligned.

    Omit<ComponentProps<"caption">, "color">

  • TableHeader

    Part. The header row group. Its rows take no hover fill.

    Omit<ComponentProps<"thead">, "color">

  • TableBody

    Part. The body row group.

    Omit<ComponentProps<"tbody">, "color">

  • TableFooter

    Part. A footer row group for totals, on a muted fill.

    Omit<ComponentProps<"tfoot">, "color">

  • TableRow

    Part. One row, with a bottom border and a muted fill on hover.

    Omit<ComponentProps<"tr">, "color">

  • TableHead

    Part. A header cell, in muted-foreground. Does not wrap.

    Omit<ComponentProps<"th">, "color">

  • TableCell

    Part. A data cell. Does not wrap. Pass className="whitespace-normal" to let it wrap.

    Omit<ComponentProps<"td">, "color">

  • className

    Extra classes on any part. Later classes win, so className="py-4" on a cell replaces its padding.

    string

Accessibility

  • Every part is the native element: <table>, <caption>, <thead>, <tbody>, <tfoot>, <tr>, <th>, and <td>. Screen readers can move by row and column and read each cell's header.
  • TableCaption names the table. Screen readers read it when they reach the table.
  • A <caption> must be the first child of its <table>, so put TableCaption first.
  • scrollLabel makes the container a role="region" with that name and tabIndex=0, so keyboard users can Tab to it and scroll with the arrow keys. It shows the shared focus ring.
  • Without scrollLabel, the container adds no tab stop and no name. Some browsers let keyboard users focus a container that scrolls. Do not count on it.
  • Header cells use muted-foreground and body text uses the page's foreground. The theme tests hold both to 4.5:1 on the page and on cards.
  • The row hover fill and the footer fill are muted at 50% over the page. Text on them sits between two pairs the theme tests hold to 4.5:1.

Examples

Invoices with a caption and a total

<Table>
  <TableCaption>Recent invoices</TableCaption>
  <TableHeader>
    <TableRow>
      <TableHead>Invoice</TableHead>
      <TableHead>Status</TableHead>
      <TableHead className="text-right">Amount</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell className="font-medium">INV-001</TableCell>
      <TableCell>Paid</TableCell>
      <TableCell className="text-right">$250.00</TableCell>
    </TableRow>
    <TableRow>
      <TableCell className="font-medium">INV-002</TableCell>
      <TableCell>Pending</TableCell>
      <TableCell className="text-right">$150.00</TableCell>
    </TableRow>
  </TableBody>
  <TableFooter>
    <TableRow>
      <TableCell colSpan={2}>Total</TableCell>
      <TableCell className="text-right">$400.00</TableCell>
    </TableRow>
  </TableFooter>
</Table>

A wide table on a phone

<Table scrollLabel="Team members" size="sm">
  <TableCaption>Team members</TableCaption>
  <TableHeader>
    <TableRow>
      <TableHead>Name</TableHead>
      <TableHead>Email</TableHead>
      <TableHead>Role</TableHead>
      <TableHead>Last active</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell>Ada Lovelace</TableCell>
      <TableCell>ada@example.com</TableCell>
      <TableCell>Owner</TableCell>
      <TableCell>2 minutes ago</TableCell>
    </TableRow>
  </TableBody>
</Table>

Row headers

<Table>
  <TableCaption>Plan limits</TableCaption>
  <TableHeader>
    <TableRow>
      <TableHead>Plan</TableHead>
      <TableHead className="text-right">Seats</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableHead scope="row">Free</TableHead>
      <TableCell className="text-right">3</TableCell>
    </TableRow>
    <TableRow>
      <TableHead scope="row">Pro</TableHead>
      <TableCell className="text-right">25</TableCell>
    </TableRow>
  </TableBody>
</Table>

No rows yet

<Table>
  <TableCaption>Orders</TableCaption>
  <TableHeader>
    <TableRow>
      <TableHead>Order</TableHead>
      <TableHead>Customer</TableHead>
      <TableHead className="text-right">Total</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell colSpan={3} className="h-24 text-center text-muted-foreground">
        No orders yet.
      </TableCell>
    </TableRow>
  </TableBody>
</Table>