1
ComponentsData Display

Table

Semantic tabular data with structured headers, cells, selection and interactive-row support.

Purpose

Use Table for genuinely tabular, scannable data whose columns need semantic headers and consistent alignment.

Card table

Recent invoices
CustomerStatusAmount
Acme Co.Paid$2,400
NorthwindOpen$1,280
Linear LabsDraft$860
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";

export function Example() {
  return (
    <Table>
      <TableHeader><TableRow><TableHead>Customer</TableHead></TableRow></TableHeader>
      <TableBody><TableRow><TableCell>Acme Co.</TableCell></TableRow></TableBody>
    </Table>
  );
}

Installation

With the Siteplane registry alias configured, add the component and import the installed source from your app.

CLI

pnpm dlx shadcn@4.16.1 add @siteplane/table

Import

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

Need the machine-readable source metadata? View registry JSON.

Usage

Use Table for true tabular data. Use DataTable when the table needs managed sorting.

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

export function Example() {
  return (
    <Table>
      <TableHeader><TableRow><TableHead>Customer</TableHead></TableRow></TableHeader>
      <TableBody><TableRow><TableCell>Acme Co.</TableCell></TableRow></TableBody>
    </Table>
  );
}

Examples

Captions, footers, row states, selection, loading and empty rows are compositions of the same slots.

Default

CustomerStatusAmount
Acme Co.Paid$2,400
NorthwindOpen$1,280
Linear LabsDraft$860
<Table>
  <TableHeader>
    <TableRow>
      <TableHead>Customer</TableHead>
      <TableHead>Status</TableHead>
      <TableHead className="text-right">Amount</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    {invoiceRows.map((row) => (
      <TableRow key={row.customer}>
        <TableCell className="font-medium">{row.customer}</TableCell>
        <TableCell>{row.status}</TableCell>
        <TableCell className="text-right">{row.amount}</TableCell>
      </TableRow>
    ))}
  </TableBody>
</Table>

With caption

TableCaption describes the table for assistive technology and sighted readers.

A list of recent invoices.
CustomerStatusAmount
Acme Co.Paid$2,400
NorthwindOpen$1,280
Linear LabsDraft$860
<Table>
  <TableCaption>A list of recent invoices.</TableCaption>
  <TableHeader>...</TableHeader>
  <TableBody>...</TableBody>
</Table>

Selected row

The native data-state="selected" row state highlights a row without extra markup.

CustomerStatusAmount
Acme Co.Paid$2,400
NorthwindOpen$1,280
Linear LabsDraft$860
<TableRow data-state="selected">
  <TableCell className="font-medium">Northwind</TableCell>
  <TableCell>Open</TableCell>
  <TableCell className="text-right">$1,280</TableCell>
</TableRow>

Row selection

Row selection is a local composition: checkboxes drive the selected row state, an indeterminate select-all and the count.

CustomerStatusAmount
Acme Co.Paid$2,400
NorthwindOpen$1,280
Linear LabsDraft$860
Total$4,540
Selected rows: 1
const [selectedIds, setSelectedIds] = useState<string[]>(["Acme Co."]);
const allSelected = selectedIds.length === billingRows.length;
const someSelected = selectedIds.length > 0 && !allSelected;

<Table variant="card">
  <TableHeader>
    <TableRow>
      <TableHead>
        <Checkbox
          aria-label="Select all invoices"
          checked={allSelected}
          indeterminate={someSelected}
          onCheckedChange={(value) => setAllSelected(value === true)}
        />
      </TableHead>
      ...
    </TableRow>
  </TableHeader>
  <TableBody>
    {billingRows.map((row) => (
      <TableRow
        data-state={selectedIds.includes(row.customer) ? "selected" : undefined}
        key={row.customer}
      >
        <TableCell>
          <Checkbox
            aria-label={`Select ${row.customer}`}
            checked={selectedIds.includes(row.customer)}
            onCheckedChange={(value) => setRowSelected(row.customer, value === true)}
          />
        </TableCell>
        ...
      </TableRow>
    ))}
  </TableBody>
  <TableFooter>...</TableFooter>
</Table>

Loading rows

CustomerStatusAmount
<TableBody>
  {loadingRowIds.map((rowId) => (
    <TableRow key={rowId}>
      {loadingSkeletonWidths.map((width) => (
        <TableCell key={`${rowId}-${width}`}>
          <Skeleton aria-hidden="true" className={width} />
        </TableCell>
      ))}
    </TableRow>
  ))}
</TableBody>

Empty row

CustomerStatusAmount
No invoicesCreate an invoice to start filling this table.
<TableBody>
  <TableRow>
    <TableCell className="h-32 text-center" colSpan={3}>
      <div className="flex flex-col items-center justify-center gap-2">
        <span className="font-medium text-sm">No invoices</span>
        <span className="text-muted-foreground text-xs">
          Create an invoice to start filling this table.
        </span>
        <Button variant="outline">Create invoice</Button>
      </div>
    </TableCell>
  </TableRow>
</TableBody>

Inside CardFrame

CardFrame gives a card-variant table an app-surface frame without touching the table itself.

CustomerStatusAmount
Acme Co.Paid$2,400
NorthwindOpen$1,280
Linear LabsDraft$860
<CardFrame>
  <Table variant="card">
    <TableHeader>...</TableHeader>
    <TableBody>...</TableBody>
  </Table>
</CardFrame>

API Reference

The reference lists the source-owned exports and props that are easy to miss in visual examples. Inherited Base UI props remain available unless the wrapper narrows them.

  • Compose Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell and TableCaption.
  • Table accepts variant="default" | "card"; the wrapper provides horizontal overflow through its native table container.
  • Set data-state="selected" on a selected row. Set data-interactive="true" on a clickable row to receive the native pointer and focus-visible treatment.
  • Use Data Table when sorting, accessor columns or controlled table behavior is required.

Motion

Component motion uses the shared Siteplane motion contract. See the global motion guide for provider setup, tokens and reduced-motion behavior.

  • Table has no mount or row-entry animation. Hover, selected and interactive states use quiet tokenized state changes.
  • Reduced motion needs no extra handling because rows do not translate or scale.

Accessibility

  • Use real header cells, body cells and a caption when the surrounding context does not already name the table.
  • Interactive rows need keyboard activation and a clear focus target; nested buttons or links must remain independently operable.
  • Loading and empty rows should span the full column count.

Implementation Guidance

  • Do not use Table as a general layout grid.
  • Use data-interactive="true" instead of rebuilding clickable-row cursor and focus styles.
  • Keep sorting and accessor logic in Data Table, not in the primitive Table.

On This Page