# Table

A table for rows of data.

Source: [table.tsx](https://github.com/jakejarvis/jarvis-ui/blob/main/src/components/ui/table.tsx)

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

const statuses = {
  paid: { label: "Paid", variant: "success" },
  sent: { label: "Sent", variant: "info" },
  pending: { label: "Pending", variant: "warning" },
  overdue: { label: "Overdue", variant: "destructive" },
  draft: { label: "Draft", variant: "neutral" },
} as const;

const invoices = [
  { id: "INV-1042", customer: "Acme Corp", status: "paid", issued: "Mar 2", amount: 6300 },
  { id: "INV-1043", customer: "Bluebird Studio", status: "sent", issued: "Mar 5", amount: 1800 },
  { id: "INV-1044", customer: "Cedar & Pine", status: "pending", issued: "Mar 9", amount: 1275 },
  { id: "INV-1045", customer: "Driftwood Labs", status: "overdue", issued: "Feb 14", amount: 4500 },
  { id: "INV-1046", customer: "Evergreen Co.", status: "draft", issued: "Mar 12", amount: 600 },
] as const;

const currency = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });

export default function TableInvoices() {
  const total = invoices.reduce((sum, invoice) => sum + invoice.amount, 0);

  return (
    <Table>
      <TableCaption>Invoices issued this quarter.</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead>Invoice</TableHead>
          <TableHead>Customer</TableHead>
          <TableHead>Status</TableHead>
          <TableHead>Issued</TableHead>
          <TableHead className="text-right">Amount</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {invoices.map((invoice) => (
          <TableRow key={invoice.id}>
            <TableCell className="font-mono">{invoice.id}</TableCell>
            <TableCell className="font-medium">{invoice.customer}</TableCell>
            <TableCell>
              <Status variant={statuses[invoice.status].variant}>
                {statuses[invoice.status].label}
              </Status>
            </TableCell>
            <TableCell className="text-muted-foreground">{invoice.issued}</TableCell>
            <TableCell className="text-right">{currency.format(invoice.amount)}</TableCell>
          </TableRow>
        ))}
      </TableBody>
      <TableFooter>
        <TableRow>
          <TableCell colSpan={4}>Total</TableCell>
          <TableCell className="text-right text-foreground">{currency.format(total)}</TableCell>
        </TableRow>
      </TableFooter>
    </Table>
  );
}
```

## Installation

Add the registry, once per project:

```bash
npx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json
```

Then add the component:

```bash
npx shadcn@latest add @jarvis/table
```

## Usage

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

export function InvoiceTable() {
  return (
    <Table>
      <TableCaption>Invoices issued this quarter.</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead>Invoice</TableHead>
          <TableHead className="text-right">Amount</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        <TableRow>
          <TableCell>INV-1042</TableCell>
          <TableCell className="text-right">$6,300.00</TableCell>
        </TableRow>
      </TableBody>
      <TableFooter>
        <TableRow>
          <TableCell>Total</TableCell>
          <TableCell className="text-right">$6,300.00</TableCell>
        </TableRow>
      </TableFooter>
    </Table>
  );
}
```

Wide tables scroll sideways inside a [Scroll Area](https://ui.jarv.is/components/scroll-area.md), which fades the edges.

## Composition

- `Table`
  - `TableCaption`
  - `TableHeader`
    - `TableRow`
      - `TableHead`
  - `TableBody`
    - `TableRow`
      - `TableCell`
  - `TableFooter`

## Examples

### Row selection

Set `data-state="selected"` on a selected row to highlight it. Give each checkbox an `aria-label`
that names its row.

```tsx
"use client";

import * as React from "react";

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

const members = [
  { id: "ada", name: "Ada Park", email: "ada@example.com", role: "Owner" },
  { id: "sam", name: "Sam Rivera", email: "sam@example.com", role: "Admin" },
  { id: "priya", name: "Priya Shah", email: "priya@example.com", role: "Member" },
  { id: "jonah", name: "Jonah Kim", email: "jonah@example.com", role: "Member" },
];

export default function TableSelection() {
  const [selected, setSelected] = React.useState<string[]>(["sam"]);
  const all = selected.length === members.length;

  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead className="w-8">
            <Checkbox
              aria-label="Select all members"
              checked={all}
              indeterminate={selected.length > 0 && !all}
              onCheckedChange={(checked) =>
                setSelected(checked ? members.map((member) => member.id) : [])
              }
            />
          </TableHead>
          <TableHead>Name</TableHead>
          <TableHead>Email</TableHead>
          <TableHead>Role</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {members.map((member) => {
          const isSelected = selected.includes(member.id);
          return (
            <TableRow key={member.id} data-state={isSelected ? "selected" : undefined}>
              <TableCell>
                <Checkbox
                  aria-label={`Select ${member.name}`}
                  checked={isSelected}
                  onCheckedChange={(checked) =>
                    setSelected((current) =>
                      checked ? [...current, member.id] : current.filter((id) => id !== member.id),
                    )
                  }
                />
              </TableCell>
              <TableCell className="font-medium">{member.name}</TableCell>
              <TableCell className="text-muted-foreground">{member.email}</TableCell>
              <TableCell>{member.role}</TableCell>
            </TableRow>
          );
        })}
      </TableBody>
    </Table>
  );
}
```

### Row actions

Give the column's header visually hidden text, and each menu trigger an `aria-label` that names
its row.

```tsx
import { EllipsisIcon, FileTextIcon, UploadIcon } from "lucide-react";

import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";

const invoices = [
  { id: "INV-1042", customer: "Acme Corp", amount: "$6,300.00" },
  { id: "INV-1043", customer: "Bluebird Studio", amount: "$1,800.00" },
  { id: "INV-1044", customer: "Cedar & Pine", amount: "$1,275.00" },
];

export default function TableRowActions() {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Invoice</TableHead>
          <TableHead>Customer</TableHead>
          <TableHead className="text-right">Amount</TableHead>
          <TableHead className="w-10">
            <span className="sr-only">Actions</span>
          </TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {invoices.map((invoice) => (
          <TableRow key={invoice.id}>
            <TableCell className="font-mono">{invoice.id}</TableCell>
            <TableCell className="font-medium">{invoice.customer}</TableCell>
            <TableCell className="text-right">{invoice.amount}</TableCell>
            <TableCell className="pr-2 text-right">
              <DropdownMenu>
                <DropdownMenuTrigger
                  render={
                    <Button
                      variant="ghost"
                      size="icon-sm"
                      aria-label={`Actions for ${invoice.id}`}
                    />
                  }
                >
                  <EllipsisIcon />
                </DropdownMenuTrigger>
                <DropdownMenuContent align="end">
                  <DropdownMenuItem>
                    <FileTextIcon />
                    View invoice
                  </DropdownMenuItem>
                  <DropdownMenuItem>
                    <UploadIcon />
                    Send reminder
                  </DropdownMenuItem>
                  <DropdownMenuSeparator />
                  <DropdownMenuItem variant="destructive">Void invoice</DropdownMenuItem>
                </DropdownMenuContent>
              </DropdownMenu>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}
```

## Props

`Table`'s props, including `className`, go to the `<table>`, not its scrolling wrapper.

| Part | Extends |
| --- | --- |
| `Table` | `<table>` |
| `TableHeader` | `<thead>` |
| `TableBody` | `<tbody>` |
| `TableFooter` | `<tfoot>` |
| `TableRow` | `<tr>` |
| `TableHead` | `<th>` |
| `TableCell` | `<td>` |
| `TableCaption` | `<caption>` |
