# Table

> Present data whose rows and columns have meaningful relationships.

Source: https://www.honestui.com/docs/components/table

```tsx
import { Badge } from "@/components/honest-ui/ui/badge";
import {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
  TableSectionHeader,
} from "@/components/honest-ui/ui/table";

export function TableDemo() {
  return (
    <div className="w-full min-w-0 max-w-4xl">
      <Table>
        <TableCaption>A list of current projects.</TableCaption>
        <TableHeader>
          <TableRow>
            <TableHead scope="col">Project</TableHead>
            <TableHead scope="col">Status</TableHead>
            <TableHead scope="col">Team</TableHead>
            <TableHead className="text-right" scope="col">
              Budget
            </TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          <TableSectionHeader>
            <TableHead colSpan={4} scope="colgroup">
              Active projects
            </TableHead>
          </TableSectionHeader>
          <TableRow>
            <TableHead className="font-medium text-foreground" scope="row">
              Website Redesign
            </TableHead>
            <TableCell>
              <Badge variant="outline">
                <span
                  className="size-1.5 rounded-full bg-emerald-500"
                  aria-hidden="true"
                />
                Paid
              </Badge>
            </TableCell>
            <TableCell>Frontend Team</TableCell>
            <TableCell className="text-right">$12,500</TableCell>
          </TableRow>
          <TableRow>
            <TableHead className="font-medium text-foreground" scope="row">
              Mobile App
            </TableHead>
            <TableCell>
              <Badge variant="outline">
                <span
                  className="size-1.5 rounded-full bg-muted-foreground/64"
                  aria-hidden="true"
                />
                Unpaid
              </Badge>
            </TableCell>
            <TableCell>Mobile Team</TableCell>
            <TableCell className="text-right">$8,750</TableCell>
          </TableRow>
          <TableRow>
            <TableHead className="font-medium text-foreground" scope="row">
              API Integration
            </TableHead>
            <TableCell>
              <Badge variant="outline">
                <span
                  className="size-1.5 rounded-full bg-amber-500"
                  aria-hidden="true"
                />
                Pending
              </Badge>
            </TableCell>
            <TableCell>Backend Team</TableCell>
            <TableCell className="text-right">$5,200</TableCell>
          </TableRow>
          <TableSectionHeader>
            <TableHead colSpan={4} scope="colgroup">
              Completed projects
            </TableHead>
          </TableSectionHeader>
          <TableRow>
            <TableHead className="font-medium text-foreground" scope="row">
              Database Migration
            </TableHead>
            <TableCell>
              <Badge variant="outline">
                <span
                  className="size-1.5 rounded-full bg-emerald-500"
                  aria-hidden="true"
                />
                Paid
              </Badge>
            </TableCell>
            <TableCell>DevOps Team</TableCell>
            <TableCell className="text-right">$3,800</TableCell>
          </TableRow>
          <TableRow>
            <TableHead className="font-medium text-foreground" scope="row">
              User Dashboard
            </TableHead>
            <TableCell>
              <Badge variant="outline">
                <span
                  className="size-1.5 rounded-full bg-emerald-500"
                  aria-hidden="true"
                />
                Paid
              </Badge>
            </TableCell>
            <TableCell>UX Team</TableCell>
            <TableCell className="text-right">$7,200</TableCell>
          </TableRow>
          <TableRow>
            <TableHead className="font-medium text-foreground" scope="row">
              Security Audit
            </TableHead>
            <TableCell>
              <Badge variant="outline">
                <span
                  className="size-1.5 rounded-full bg-red-500"
                  aria-hidden="true"
                />
                Failed
              </Badge>
            </TableCell>
            <TableCell>Security Team</TableCell>
            <TableCell className="text-right">$2,100</TableCell>
          </TableRow>
        </TableBody>
        <TableFooter>
          <TableRow>
            <TableCell colSpan={3}>Total Budget</TableCell>
            <TableCell className="text-right font-medium">$39,550</TableCell>
          </TableRow>
        </TableFooter>
      </Table>
    </div>
  );
}

```

## Overview [#overview]

Use Table for structured data where people compare values across rows: invoices, users, orders, logs, permissions, metrics. A table's promise is alignment — each column means the same thing on every row, so scanning down beats reading across. When each item has varied content and nobody needs to align values, use cards or lists instead.

## Anatomy [#anatomy]

A table has an optional caption, a header row, body rows, cells, an optional footer, and optional section headers. `Table` renders the native `<table>` element inside a horizontally scrollable container, so wide tables scroll rather than compressing their columns into illegibility.

## Formatting [#formatting]

Keep numeric columns right-aligned so digits line up for comparison; the component already applies tabular figures, which keeps columns of numbers from wobbling as values change. Format consistently within a column — same currency, same precision, same date format on every row. Cells clip overflow onto one line with an ellipsis rather than wrapping; see [Accessibility](#accessibility) for how to keep clipped values reachable.

## Accessibility [#accessibility]

Give the table a `TableCaption` or connect it to a visible heading so its purpose is announced before people enter the cell maze. Set `scope="col"` on every column header and `scope="row"` on row headers (`TableHead` used inside `TableBody`); use `scope="colgroup"` with a matching `colSpan` for `TableSectionHeader`. The components pass these attributes through but do not add them for you.

Cell text truncated by the ellipsis is still announced fully by screen readers — clipping is visual only — but sighted users lose the ending silently. Copy the full value into a `title` attribute, or move long content out of dense tables entirely.

`interactive` on `TableRow` adds hover, active, and cursor styling only. It adds no click handler and no keyboard semantics, so an "interactive-looking" row must still expose its actions through real controls inside the cells: a link on the primary value, a menu button per row, or a checkbox for selection paired with the built-in selected state. Colors come from theme tokens, so tables adapt to dark mode automatically, and the sticky header stays legible over scrolling body rows.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;table&#x22;]" />
  

  
    
      
        Copy and paste the following code into your project.
      

      ### components/ui/table.tsx

```tsx
import * as React from "react"
import type { ComponentPropsWithoutRef } from "react"

import { cn } from "@/lib/utils"

function Table({ className, ...props }: ComponentPropsWithoutRef<"table">) {
  return (
    <div
      data-slot="table-container"
      className="relative w-full overflow-x-auto"
    >
      <table
        data-slot="table"
        className={cn(
          "w-full caption-bottom border-collapse overflow-visible text-[var(--hui-color-foreground-base-primary)] tabular-nums [font-size:var(--hui-font-size-small)] [line-height:var(--hui-line-height-small)]",
          className
        )}
        {...props}
      />
    </div>
  )
}

function TableHeader({ className, ...props }: ComponentPropsWithoutRef<"thead">) {
  return (
    <thead
      data-slot="table-header"
      className={cn(
        "sticky top-0 z-1 m-0 bg-[var(--hui-color-background-base-primary)]",
        className
      )}
      {...props}
    />
  )
}

function TableBody({ className, ...props }: ComponentPropsWithoutRef<"tbody">) {
  return (
    <tbody
      data-slot="table-body"
      className={className}
      {...props}
    />
  )
}

function TableFooter({ className, ...props }: ComponentPropsWithoutRef<"tfoot">) {
  return (
    <tfoot
      data-slot="table-footer"
      className={className}
      {...props}
    />
  )
}

function TableRow({
  className,
  interactive = false,
  ...props
}: ComponentPropsWithoutRef<"tr"> & { interactive?: boolean }) {
  return (
    <tr
      data-slot="table-row"
      className={cn(
        "bg-[var(--hui-color-background-base-primary)] [&>[data-slot=table-cell]]:bg-inherit data-[state=selected]:bg-[var(--hui-color-background-accent-primary)] data-[state=selected]:hover:bg-[var(--hui-color-background-accent-primary)] data-[state=selected]:active:bg-[var(--hui-color-background-accent-primary)]",
        interactive &&
          "cursor-pointer hover:bg-[var(--hui-color-background-base-primary-hover)] active:bg-[var(--hui-color-background-neutral-secondary)]",
        className
      )}
      {...props}
    />
  )
}

function TableHead({ className, ...props }: ComponentPropsWithoutRef<"th">) {
  return (
    <th
      data-slot="table-head"
      className={cn(
        "border-b-[0.5px] border-[var(--hui-color-border-base-primary)] bg-[var(--hui-color-background-base-primary)] p-[var(--hui-space-3)] text-left align-middle text-[var(--hui-color-foreground-base-tertiary)] [font-size:var(--hui-font-size-small)] [font-style:normal] [font-weight:var(--hui-font-weight-medium)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-small)]",
        className
      )}
      {...props}
    />
  )
}

function TableCell({ className, ...props }: ComponentPropsWithoutRef<"td">) {
  return (
    <td
      data-slot="table-cell"
      className={cn(
        "overflow-hidden border-b-[0.5px] border-[var(--hui-color-border-base-primary)] bg-[var(--hui-color-background-base-primary)] px-[var(--hui-space-3)] py-[var(--hui-space-4)] text-ellipsis whitespace-nowrap text-[var(--hui-color-foreground-base-secondary)] [font-size:var(--hui-font-size-regular)] [font-style:normal] [font-weight:var(--hui-font-weight-regular)] [letter-spacing:var(--hui-letter-spacing-regular)] [line-height:var(--hui-line-height-regular)]",
        className
      )}
      {...props}
    />
  )
}

function TableSectionHeader({
  className,
  ...props
}: ComponentPropsWithoutRef<"tr">) {
  return (
    <tr
      data-slot="table-section-header"
      className={cn(
        "bg-[var(--hui-color-background-neutral-primary)] text-left [&>th]:bg-inherit [&>th]:p-[var(--hui-space-3)] [&>th]:text-[var(--hui-color-foreground-base-primary)] [&>th]:[font-size:var(--hui-font-size-small)] [&>th]:[font-style:normal] [&>th]:[font-weight:var(--hui-font-weight-medium)] [&>th]:[letter-spacing:var(--hui-letter-spacing-small)] [&>th]:[line-height:var(--hui-line-height-small)]",
        className
      )}
      {...props}
    />
  )
}

function TableCaption({
  className,
  ...props
}: ComponentPropsWithoutRef<"caption">) {
  return (
    <caption
      data-slot="table-caption"
      className={cn(
        "mt-[var(--hui-space-4)] text-[var(--hui-color-foreground-base-secondary)] [font-size:var(--hui-font-size-small)] [line-height:var(--hui-line-height-small)]",
        className
      )}
      {...props}
    />
  )
}

export {
  Table,
  TableHeader,
  TableBody,
  TableFooter,
  TableHead,
  TableRow,
  TableSectionHeader,
  TableCell,
  TableCaption,
}

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

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

```tsx
<Table>
  <TableCaption>Caption</TableCaption>
  <TableHeader>
    <TableRow>
      <TableHead scope="col">Header</TableHead>
      <TableHead scope="col">Header</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell>Cell</TableCell>
      <TableCell>Cell</TableCell>
    </TableRow>
  </TableBody>
</Table>
```

The header sticks while the page scrolls vertically; the container scrolls horizontally when columns exceed the available width.

## Don't do this [#dont-do-this]

### Headers without scope [#headers-without-scope]

```tsx
// Bad
<TableHeader>
  <TableRow>
    <TableHead>User</TableHead>
    <TableHead>Role</TableHead>
  </TableRow>
</TableHeader>
```

```tsx
// Good
<TableHeader>
  <TableRow>
    <TableHead scope="col">User</TableHead>
    <TableHead scope="col">Role</TableHead>
  </TableRow>
</TableHeader>
```

Without explicit scopes, screen readers guess which headers describe which cells. Guessing works on tiny two-column tables and fails exactly when tables get big enough to need help — many columns, section headers, or split header rows. One attribute per header removes the guesswork.

### Rows that navigate by click alone [#rows-that-navigate-by-click-alone]

```tsx
// Bad
<TableRow interactive onClick={() => router.push(`/invoices/${id}`)}>
  <TableCell>INV-1042</TableCell>
  <TableCell>$240</TableCell>
</TableRow>
```

```tsx
// Good
<TableRow interactive>
  <TableCell>
    <Link className="underline-offset-4 hover:underline" href={`/invoices/${id}`}>
      INV-1042
    </Link>
  </TableCell>
  <TableCell>$240</TableCell>
</TableRow>
```

The click-only version cannot be focused or triggered by keyboard, cannot be opened in a new tab, and gives no hint of what clicking does. Keep the hover affordance if you like it, but put the destination on a real link — the invoice number is the natural place.

### Numbers aligned left [#numbers-aligned-left]

```tsx
// Bad
<TableHead>Amount</TableHead>
...
<TableCell>$12,500</TableCell>
```

```tsx
// Good
<TableHead className="text-right">Amount</TableHead>
...
<TableCell className="text-right">$12,500</TableCell>
```

Left-aligned amounts destroy the comparison a money column exists for: varying digit counts put commas and cents at different horizontal positions on every row. Align both the header and the cells right so magnitude reads at a glance.

## Examples [#examples]

### Framed Table [#framed-table]

The same semantic structure presented inside a bordered surface.

```tsx
import { Badge } from "@/components/honest-ui/ui/badge"
import { Frame, FramePanel } from "@/components/honest-ui/ui/frame"
import {
  Table,
  TableBody,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/honest-ui/ui/table"

export function TableFramed() {
  return (
    <Frame className="w-full min-w-0 max-w-4xl">
      <FramePanel>
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead>Project</TableHead>
              <TableHead>Status</TableHead>
              <TableHead>Team</TableHead>
              <TableHead className="text-right">Budget</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            <TableRow>
              <TableCell className="font-medium">
                Website Redesign
              </TableCell>
              <TableCell>
                <Badge variant="outline">
                  <span
                    className="size-1.5 rounded-full bg-emerald-500"
                    aria-hidden="true"
                  />
                  Paid
                </Badge>
              </TableCell>
              <TableCell>Frontend Team</TableCell>
              <TableCell className="text-right">$12,500</TableCell>
            </TableRow>
            <TableRow>
              <TableCell className="font-medium">Mobile App</TableCell>
              <TableCell>
                <Badge variant="outline">
                  <span
                    className="size-1.5 rounded-full bg-muted-foreground/64"
                    aria-hidden="true"
                  />
                  Unpaid
                </Badge>
              </TableCell>
              <TableCell>Mobile Team</TableCell>
              <TableCell className="text-right">$8,750</TableCell>
            </TableRow>
            <TableRow>
              <TableCell className="font-medium">
                API Integration
              </TableCell>
              <TableCell>
                <Badge variant="outline">
                  <span
                    className="size-1.5 rounded-full bg-amber-500"
                    aria-hidden="true"
                  />
                  Pending
                </Badge>
              </TableCell>
              <TableCell>Backend Team</TableCell>
              <TableCell className="text-right">$5,200</TableCell>
            </TableRow>
            <TableRow>
              <TableCell className="font-medium">
                Database Migration
              </TableCell>
              <TableCell>
                <Badge variant="outline">
                  <span
                    className="size-1.5 rounded-full bg-emerald-500"
                    aria-hidden="true"
                  />
                  Paid
                </Badge>
              </TableCell>
              <TableCell>DevOps Team</TableCell>
              <TableCell className="text-right">$3,800</TableCell>
            </TableRow>
            <TableRow>
              <TableCell className="font-medium">
                User Dashboard
              </TableCell>
              <TableCell>
                <Badge variant="outline">
                  <span
                    className="size-1.5 rounded-full bg-emerald-500"
                    aria-hidden="true"
                  />
                  Paid
                </Badge>
              </TableCell>
              <TableCell>UX Team</TableCell>
              <TableCell className="text-right">$7,200</TableCell>
            </TableRow>
            <TableRow>
              <TableCell className="font-medium">
                Security Audit
              </TableCell>
              <TableCell>
                <Badge variant="outline">
                  <span
                    className="size-1.5 rounded-full bg-red-500"
                    aria-hidden="true"
                  />
                  Failed
                </Badge>
              </TableCell>
              <TableCell>Security Team</TableCell>
              <TableCell className="text-right">$2,100</TableCell>
            </TableRow>
          </TableBody>
          <TableFooter>
            <TableRow>
              <TableCell colSpan={3}>Total Budget</TableCell>
              <TableCell className="text-right font-medium">$39,550</TableCell>
            </TableRow>
          </TableFooter>
        </Table>
      </FramePanel>
    </Frame>
  )
}

```

### Invoice list [#invoice-list]

Badged status, right-aligned amounts, scoped headers.

```tsx
import { Badge } from "@/components/honest-ui/ui/badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/honest-ui/ui/table"

export function TableInvoiceList() {
  return (
    <div className="w-full max-w-md">
      <Table className="text-sm">
        <TableHeader><TableRow><TableHead>Invoice</TableHead><TableHead>Status</TableHead><TableHead className="text-right">Amount</TableHead></TableRow></TableHeader>
        <TableBody>
          <TableRow><TableCell>INV-1042</TableCell><TableCell><Badge variant="success">Paid</Badge></TableCell><TableCell className="text-right">$240</TableCell></TableRow>
          <TableRow><TableCell>INV-1043</TableCell><TableCell><Badge variant="warning">Due</Badge></TableCell><TableCell className="text-right">$180</TableCell></TableRow>
        </TableBody>
      </Table>
    </div>
  )
}

```

### User roles [#user-roles]

A minimal two-column readout.

```tsx
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/honest-ui/ui/table"

const users = [["Nora", "Admin"], ["Leo", "Editor"], ["Mia", "Viewer"]]

export function TableUserRoles() {
  return (
    <div className="w-full max-w-sm">
      <Table className="text-sm">
        <TableHeader><TableRow><TableHead>User</TableHead><TableHead>Role</TableHead></TableRow></TableHeader>
        <TableBody>{users.map(([user, role]) => <TableRow key={user}><TableCell>{user}</TableCell><TableCell>{role}</TableCell></TableRow>)}</TableBody>
      </Table>
    </div>
  )
}

```

## API reference [#api-reference]

All Table parts accept props for their matching native HTML elements — `<table>`, `<thead>`, `<tbody>`, `<tfoot>`, `<tr>`, `<th>`, `<td>`, `<caption>` — plus one addition:

| Part       | Prop          | Values  | Default |
| ---------- | ------------- | ------- | ------- |
| `TableRow` | `interactive` | boolean | `false` |

Notes:

* `Table` wraps the native table in a horizontally scrollable container; the table itself uses `caption-bottom` and tabular figures.
* `TableHeader` renders a sticky header (`top-0`) against vertical page scroll.
* `TableSectionHeader` renders a `tr`; place one or more `TableHead` cells inside it with a `colSpan` covering the table's width and `scope="colgroup"` for correct announcement.
* `data-state="selected"` on a `TableRow` applies the selected background, ready for controlled row selection.
