# Shadcn Table

> Free shadcn/ui table component for React — the styled <table> primitive plus sortable, selectable, sticky-header, paginated, and expandable examples, all in plain React state (no TanStack). The examples official shadcn never shipped.

Source: https://designrevision.com/components/table

The **Shadcn Table** is the styled `<table>` — the canonical <a href="https://ui.shadcn.com/docs/components/table" rel="nofollow">shadcn/ui</a> table built from eight semantic parts with **no dependency**. Official ships the primitive and a single invoices demo; the examples below make it do real work — **sort, select, paginate, and expand** — all in plain React state, no TanStack required.

## Table vs Data Table

This is the most common question, so up front:

- **Table** (this page) is the **static, styled markup** — `Table`, `TableHeader`, `TableBody`, `TableFooter`, `TableRow`, `TableHead`, `TableCell`, `TableCaption`. You bring the rows. It's plain HTML, so it's tiny and works anywhere.
- **Data Table** is the Table driven by **<a href="https://tanstack.com/table" rel="nofollow">TanStack Table</a>** (`@tanstack/react-table`) — one engine that adds sorting, filtering, global search, pagination, row selection, and column visibility over large datasets.

The key insight: **you rarely need TanStack.** Striping, sticky headers, row selection with select-all, click-to-sort, client-side pagination, and expandable rows all work with a few lines of `useState` — every example below proves it. Reach for a Data Table when you have large data, **server-side** sorting/filtering/pagination, or faceted filters.

## Striping, borders & density

The base table is borderless rows with a hover tint. **Stripe** it with `odd:bg-muted/50` on each `TableRow` (the Striped rows example). For a bordered card look, wrap the table in `rounded-md border`; for denser rows, add `[&_td]:py-1` to the `Table`. Right-align numeric columns with `text-right tabular-nums` so digits line up — see the totals row in the basic example.

## Selectable rows

Row selection doesn't need a table library. Keep an array of selected ids, put a [Checkbox](/components/checkbox) in each row, and give the header a checkbox whose `checked` is `true`, `false`, or `"indeterminate"` based on the count. Set `data-state="selected"` on the chosen `TableRow` and the primitive highlights it for you. The Selectable rows example is the full pattern, including the indeterminate select-all.

## Sortable columns

Hold a `{ key, dir }` state, render each header as a button that toggles it, and sort a copy of the data with `useMemo` before mapping. An arrow icon shows the active column and direction. The Sortable headers example does client-side sort with zero dependencies.

## Status badges & row actions

Tables are where status lives. Render a [Badge](/components/badge) in a cell with a variant per status (paid / pending / refunded), and add a trailing actions column of ghost icon-[buttons](/components/button) (edit, delete). The Status & row actions example is the orders-table layout.

## Sticky header & scrolling

For long tables the trick is *where* the scroll box lives. The `Table` primitive already wraps your `<table>` in an `overflow-x-auto` container, and (per the CSS spec) that container scrolls **vertically** too — so the height belongs on **that** container, not an outer div, or the sticky header has nothing to stick to:

```tsx
<div className="[&_[data-slot=table-container]]:max-h-[320px]">
  <Table>
    <TableHeader className="[&_th]:bg-background [&_th]:sticky [&_th]:top-0">
```

The `bg-background` is essential so scrolling rows don't bleed through the header. The Sticky header example shows it in a bordered, scrollable card.

## Pagination

Client-side pagination is a `slice`: track `page` and `pageSize`, compute `pageCount`, and render a rows-per-page [Select](/components/select) with prev/next buttons. No library, no server round-trip — ideal for a few hundred rows. The Pagination example is copy-paste ready.

## Expandable rows

For master/detail, track which row id is open and render a second `TableRow` with a `colSpan` detail cell beneath it when expanded. A chevron rotates to show state. The Expandable rows example implements the collapsible pattern in plain state.

## Accessibility

The Table is real semantic HTML — `<table>`, `<thead>`, `<tbody>`, `<th>`, `<td>` — so screen readers and keyboard users get correct table semantics for free. Use `TableCaption` to describe the table, give icon-only action buttons an `aria-label`, and label selection checkboxes. Keep one logical header row so column associations stay intact.

## Theming

The table reads your design tokens — `--border` (row dividers), `--muted` (hover, stripes, footer), `--foreground` — so it follows your theme, including dark mode, with no overrides. Style any part with a className to adjust padding, alignment, or width per column.

## Installation

### table

**Install:**

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

**Dependencies:** utils

**Usage & accessibility:** Eight styled table parts (Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption) — plain semantic HTML, no dependency. Table wraps the <table> in an overflow-x-auto container; TableRow highlights on data-[state=selected]. For sorting, filtering, pagination, and row selection at scale, drive it with TanStack Table (the Data Table pattern); for moderate data, plain React state is enough (see the examples).

```tsx
// components/ui/table.tsx
"use client"

import * as React from "react"

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

function Table({ className, ...props }: React.ComponentProps<"table">) {
  return (
    <div
      data-slot="table-container"
      className="relative w-full overflow-x-auto"
    >
      <table
        data-slot="table"
        className={cn("w-full caption-bottom text-sm", className)}
        {...props}
      />
    </div>
  )
}

function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
  return (
    <thead
      data-slot="table-header"
      className={cn("[&_tr]:border-b", className)}
      {...props}
    />
  )
}

function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
  return (
    <tbody
      data-slot="table-body"
      className={cn("[&_tr:last-child]:border-0", className)}
      {...props}
    />
  )
}

function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
  return (
    <tfoot
      data-slot="table-footer"
      className={cn(
        "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
        className
      )}
      {...props}
    />
  )
}

function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
  return (
    <tr
      data-slot="table-row"
      className={cn(
        "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
        className
      )}
      {...props}
    />
  )
}

function TableHead({ className, ...props }: React.ComponentProps<"th">) {
  return (
    <th
      data-slot="table-head"
      className={cn(
        "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
        className
      )}
      {...props}
    />
  )
}

function TableCell({ className, ...props }: React.ComponentProps<"td">) {
  return (
    <td
      data-slot="table-cell"
      className={cn(
        "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
        className
      )}
      {...props}
    />
  )
}

function TableCaption({
  className,
  ...props
}: React.ComponentProps<"caption">) {
  return (
    <caption
      data-slot="table-caption"
      className={cn("text-muted-foreground mt-4 text-sm", className)}
      {...props}
    />
  )
}

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

## Examples

### Table

The canonical invoices table — caption, header, body, and a footer totals row with right-aligned currency.

**Install:**

```bash
npx shadcn@latest add @designrevision/table-demo-01
```

**Dependencies:** table

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

const invoices = [
  { invoice: "INV001", status: "Paid", method: "Credit Card", amount: "$250.00" },
  { invoice: "INV002", status: "Pending", method: "PayPal", amount: "$150.00" },
  { invoice: "INV003", status: "Unpaid", method: "Bank Transfer", amount: "$350.00" },
  { invoice: "INV004", status: "Paid", method: "Credit Card", amount: "$450.00" },
  { invoice: "INV005", status: "Paid", method: "PayPal", amount: "$550.00" },
]

export default function TableDemo01() {
  return (
    <Table>
      <TableCaption>A list of your recent invoices.</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead className="w-[100px]">Invoice</TableHead>
          <TableHead>Status</TableHead>
          <TableHead>Method</TableHead>
          <TableHead className="text-right">Amount</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {invoices.map((invoice) => (
          <TableRow key={invoice.invoice}>
            <TableCell className="font-medium">{invoice.invoice}</TableCell>
            <TableCell>{invoice.status}</TableCell>
            <TableCell>{invoice.method}</TableCell>
            <TableCell className="text-right tabular-nums">{invoice.amount}</TableCell>
          </TableRow>
        ))}
      </TableBody>
      <TableFooter>
        <TableRow>
          <TableCell colSpan={3}>Total</TableCell>
          <TableCell className="text-right tabular-nums">$1,750.00</TableCell>
        </TableRow>
      </TableFooter>
    </Table>
  )
}
```

---

### Striped rows

Zebra striping with the `odd:bg-muted/50` utility on each row — no extra markup.

**Install:**

```bash
npx shadcn@latest add @designrevision/table-striped-01
```

**Dependencies:** table

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

const people = [
  { name: "Ada Lovelace", title: "Mathematician", team: "Engineering" },
  { name: "Alan Turing", title: "Computer Scientist", team: "Research" },
  { name: "Grace Hopper", title: "Rear Admiral", team: "Engineering" },
  { name: "Katherine Johnson", title: "Mathematician", team: "Research" },
  { name: "Margaret Hamilton", title: "Software Engineer", team: "Apollo" },
  { name: "Hedy Lamarr", title: "Inventor", team: "Comms" },
]

export default function TableStriped01() {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Name</TableHead>
          <TableHead>Title</TableHead>
          <TableHead>Team</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {people.map((person) => (
          <TableRow
            key={person.name}
            className="odd:bg-muted/50 odd:hover:bg-muted/50"
          >
            <TableCell className="font-medium">{person.name}</TableCell>
            <TableCell>{person.title}</TableCell>
            <TableCell>{person.team}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

---

### Selectable rows

A checkbox column with an **indeterminate select-all** — plain React state, no TanStack. Uses [Checkbox](/components/checkbox).

**Install:**

```bash
npx shadcn@latest add @designrevision/table-selectable-01
```

**Dependencies:** table, checkbox

```tsx
// components/ui/table-selectable-01.tsx
"use client"

import * as React from "react"

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

const people = [
  { id: "1", name: "Ada Lovelace", email: "ada@example.com", role: "Owner" },
  { id: "2", name: "Alan Turing", email: "alan@example.com", role: "Member" },
  { id: "3", name: "Grace Hopper", email: "grace@example.com", role: "Member" },
  { id: "4", name: "Katherine Johnson", email: "kj@example.com", role: "Viewer" },
]

export default function TableSelectable01() {
  const [selected, setSelected] = React.useState<string[]>([])
  const allSelected = selected.length === people.length
  const someSelected = selected.length > 0 && !allSelected

  const toggleAll = (value: boolean) =>
    setSelected(value ? people.map((person) => person.id) : [])
  const toggleOne = (id: string, value: boolean) =>
    setSelected((prev) =>
      value ? [...prev, id] : prev.filter((item) => item !== id)
    )

  return (
    <Table>
      <TableCaption>
        {selected.length} of {people.length} row(s) selected.
      </TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead className="w-[40px]">
            <Checkbox
              checked={allSelected ? true : someSelected ? "indeterminate" : false}
              onCheckedChange={(value) => toggleAll(value === true)}
              aria-label="Select all"
            />
          </TableHead>
          <TableHead>Name</TableHead>
          <TableHead>Email</TableHead>
          <TableHead>Role</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {people.map((person) => {
          const checked = selected.includes(person.id)
          return (
            <TableRow
              key={person.id}
              data-state={checked ? "selected" : undefined}
            >
              <TableCell>
                <Checkbox
                  checked={checked}
                  onCheckedChange={(value) => toggleOne(person.id, value === true)}
                  aria-label={`Select ${person.name}`}
                />
              </TableCell>
              <TableCell className="font-medium">{person.name}</TableCell>
              <TableCell>{person.email}</TableCell>
              <TableCell>{person.role}</TableCell>
            </TableRow>
          )
        })}
      </TableBody>
    </Table>
  )
}
```

---

### Sortable headers

Click a column header to sort ascending / descending — client-side sort with plain `useState`.

**Install:**

```bash
npx shadcn@latest add @designrevision/table-sortable-01
```

**Dependencies:** lucide-react, table, button

```tsx
// components/ui/table-sortable-01.tsx
"use client"

import * as React from "react"
import { ArrowDownIcon, ArrowUpDownIcon, ArrowUpIcon } from "lucide-react"

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

type Person = { name: string; title: string; age: number }

const data: Person[] = [
  { name: "Ada Lovelace", title: "Mathematician", age: 36 },
  { name: "Alan Turing", title: "Computer Scientist", age: 41 },
  { name: "Grace Hopper", title: "Rear Admiral", age: 85 },
  { name: "Katherine Johnson", title: "Mathematician", age: 101 },
]

type SortKey = keyof Person

export default function TableSortable01() {
  const [sort, setSort] = React.useState<{ key: SortKey; dir: "asc" | "desc" }>({
    key: "name",
    dir: "asc",
  })

  const sorted = React.useMemo(() => {
    return [...data].sort((a, b) => {
      if (a[sort.key] < b[sort.key]) return sort.dir === "asc" ? -1 : 1
      if (a[sort.key] > b[sort.key]) return sort.dir === "asc" ? 1 : -1
      return 0
    })
  }, [sort])

  const toggle = (key: SortKey) =>
    setSort((prev) =>
      prev.key === key
        ? { key, dir: prev.dir === "asc" ? "desc" : "asc" }
        : { key, dir: "asc" }
    )

  function SortHeader({ column, label }: { column: SortKey; label: string }) {
    const active = sort.key === column
    return (
      <Button
        variant="ghost"
        size="sm"
        onClick={() => toggle(column)}
        className="-ml-3 h-8"
      >
        {label}
        {active ? (
          sort.dir === "asc" ? (
            <ArrowUpIcon />
          ) : (
            <ArrowDownIcon />
          )
        ) : (
          <ArrowUpDownIcon className="opacity-50" />
        )}
      </Button>
    )
  }

  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>
            <SortHeader column="name" label="Name" />
          </TableHead>
          <TableHead>
            <SortHeader column="title" label="Title" />
          </TableHead>
          <TableHead className="text-right">
            <SortHeader column="age" label="Age" />
          </TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {sorted.map((person) => (
          <TableRow key={person.name}>
            <TableCell className="font-medium">{person.name}</TableCell>
            <TableCell>{person.title}</TableCell>
            <TableCell className="text-right tabular-nums">{person.age}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

---

### Status & row actions

Status [badges](/components/badge) in cells plus per-row ghost icon-button actions — the orders-table pattern.

**Install:**

```bash
npx shadcn@latest add @designrevision/table-status-01
```

**Dependencies:** lucide-react, table, badge, button, dropdown-menu

```tsx
// components/ui/table-status-01.tsx
"use client"

import { CopyIcon, MoreHorizontalIcon, PencilIcon, Trash2Icon } from "lucide-react"

import { Badge } from "@/components/ui/badge"
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 orders = [
  { id: "#3201", customer: "Ada Lovelace", status: "Paid", total: "$250.00" },
  { id: "#3202", customer: "Alan Turing", status: "Pending", total: "$150.00" },
  { id: "#3203", customer: "Grace Hopper", status: "Refunded", total: "$350.00" },
  { id: "#3204", customer: "Katherine Johnson", status: "Paid", total: "$450.00" },
]

const statusVariant: Record<
  string,
  "default" | "secondary" | "destructive" | "outline"
> = {
  Paid: "default",
  Pending: "secondary",
  Refunded: "destructive",
}

export default function TableStatus01() {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Order</TableHead>
          <TableHead>Customer</TableHead>
          <TableHead>Status</TableHead>
          <TableHead className="text-right">Total</TableHead>
          <TableHead className="w-[80px]">
            <span className="sr-only">Actions</span>
          </TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {orders.map((order) => (
          <TableRow key={order.id}>
            <TableCell className="font-medium">{order.id}</TableCell>
            <TableCell>{order.customer}</TableCell>
            <TableCell>
              <Badge variant={statusVariant[order.status]}>{order.status}</Badge>
            </TableCell>
            <TableCell className="text-right tabular-nums">{order.total}</TableCell>
            <TableCell className="text-right">
              <DropdownMenu>
                <DropdownMenuTrigger asChild>
                  <Button
                    variant="ghost"
                    size="icon"
                    className="data-[state=open]:bg-muted size-8"
                    aria-label="Open menu"
                  >
                    <MoreHorizontalIcon />
                  </Button>
                </DropdownMenuTrigger>
                <DropdownMenuContent align="end">
                  <DropdownMenuItem>
                    <PencilIcon />
                    Edit
                  </DropdownMenuItem>
                  <DropdownMenuItem>
                    <CopyIcon />
                    Duplicate
                  </DropdownMenuItem>
                  <DropdownMenuSeparator />
                  <DropdownMenuItem variant="destructive">
                    <Trash2Icon />
                    Delete
                  </DropdownMenuItem>
                </DropdownMenuContent>
              </DropdownMenu>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

---

### Sticky header

A scrollable table whose header stays pinned with `position: sticky` on the header cells.

**Install:**

```bash
npx shadcn@latest add @designrevision/table-sticky-01
```

**Dependencies:** table

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

const rows = Array.from({ length: 20 }, (_, i) => ({
  id: i + 1,
  name: `User ${i + 1}`,
  email: `user${i + 1}@example.com`,
  joined: `2026-${String((i % 12) + 1).padStart(2, "0")}-12`,
}))

export default function TableSticky01() {
  return (
    <div className="overflow-hidden rounded-md border [&_[data-slot=table-container]]:max-h-[320px]">
      <Table>
        <TableHeader className="[&_th]:bg-background [&_th]:sticky [&_th]:top-0 [&_th]:z-10">
          <TableRow>
            <TableHead className="w-[60px]">#</TableHead>
            <TableHead>Name</TableHead>
            <TableHead>Email</TableHead>
            <TableHead className="text-right">Joined</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((row) => (
            <TableRow key={row.id}>
              <TableCell className="text-muted-foreground">{row.id}</TableCell>
              <TableCell className="font-medium">{row.name}</TableCell>
              <TableCell>{row.email}</TableCell>
              <TableCell className="text-right tabular-nums">
                {row.joined}
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  )
}
```

---

### Pagination

A rows-per-page [Select](/components/select) and prev/next controls slicing the data in state — no TanStack.

**Install:**

```bash
npx shadcn@latest add @designrevision/table-pagination-01
```

**Dependencies:** lucide-react, table, button, @designrevision/select

```tsx
// components/ui/table-pagination-01.tsx
"use client"

import * as React from "react"
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const data = Array.from({ length: 23 }, (_, i) => ({
  id: i + 1,
  name: `Product ${i + 1}`,
  category: ["Apparel", "Books", "Electronics"][i % 3],
  price: `$${(9.99 + i).toFixed(2)}`,
}))

export default function TablePagination01() {
  const [pageSize, setPageSize] = React.useState(5)
  const [page, setPage] = React.useState(0)

  const pageCount = Math.ceil(data.length / pageSize)
  const start = page * pageSize
  const rows = data.slice(start, start + pageSize)

  React.useEffect(() => {
    setPage(0)
  }, [pageSize])

  return (
    <div className="space-y-3">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead className="w-[60px]">#</TableHead>
            <TableHead>Name</TableHead>
            <TableHead>Category</TableHead>
            <TableHead className="text-right">Price</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((row) => (
            <TableRow key={row.id}>
              <TableCell className="text-muted-foreground">{row.id}</TableCell>
              <TableCell className="font-medium">{row.name}</TableCell>
              <TableCell>{row.category}</TableCell>
              <TableCell className="text-right tabular-nums">{row.price}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
      <div className="flex flex-wrap items-center justify-between gap-4">
        <div className="flex items-center gap-2 text-sm text-muted-foreground">
          <span>Rows per page</span>
          <Select
            value={String(pageSize)}
            onValueChange={(value) => setPageSize(Number(value))}
          >
            <SelectTrigger className="h-8 w-[70px]">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {[5, 10, 20].map((n) => (
                <SelectItem key={n} value={String(n)}>
                  {n}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>
        <div className="flex items-center gap-2">
          <span className="text-sm text-muted-foreground">
            Page {page + 1} of {pageCount}
          </span>
          <Button
            variant="outline"
            size="icon"
            className="size-8"
            onClick={() => setPage((p) => Math.max(0, p - 1))}
            disabled={page === 0}
            aria-label="Previous page"
          >
            <ChevronLeftIcon />
          </Button>
          <Button
            variant="outline"
            size="icon"
            className="size-8"
            onClick={() => setPage((p) => Math.min(pageCount - 1, p + 1))}
            disabled={page >= pageCount - 1}
            aria-label="Next page"
          >
            <ChevronRightIcon />
          </Button>
        </div>
      </div>
    </div>
  )
}
```

---

### Expandable rows

Click a row to reveal a detail row beneath it — a collapsible master/detail table in plain state.

**Install:**

```bash
npx shadcn@latest add @designrevision/table-expandable-01
```

**Dependencies:** lucide-react, table, button

```tsx
// components/ui/table-expandable-01.tsx
"use client"

import * as React from "react"
import { ChevronRightIcon } from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const orders = [
  {
    id: "#3201",
    customer: "Ada Lovelace",
    total: "$250.00",
    address: "12 Analytical Way",
    items: ["Mechanical keyboard", "Wireless mouse"],
  },
  {
    id: "#3202",
    customer: "Alan Turing",
    total: "$150.00",
    address: "1 Enigma Road",
    items: ["Leather notebook"],
  },
  {
    id: "#3203",
    customer: "Grace Hopper",
    total: "$350.00",
    address: "9 Compiler Street",
    items: ["27\" monitor", "USB-C cable", "Laptop stand"],
  },
]

export default function TableExpandable01() {
  const [expanded, setExpanded] = React.useState<string | null>(null)

  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead className="w-[40px]" />
          <TableHead>Order</TableHead>
          <TableHead>Customer</TableHead>
          <TableHead className="text-right">Total</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {orders.map((order) => {
          const isOpen = expanded === order.id
          return (
            <React.Fragment key={order.id}>
              <TableRow
                data-state={isOpen ? "selected" : undefined}
                className="cursor-pointer"
                onClick={() => setExpanded(isOpen ? null : order.id)}
              >
                <TableCell>
                  <Button
                    variant="ghost"
                    size="icon"
                    className="size-7"
                    aria-label={isOpen ? "Collapse row" : "Expand row"}
                  >
                    <ChevronRightIcon
                      className={cn("transition-transform", isOpen && "rotate-90")}
                    />
                  </Button>
                </TableCell>
                <TableCell className="font-medium">{order.id}</TableCell>
                <TableCell>{order.customer}</TableCell>
                <TableCell className="text-right tabular-nums">
                  {order.total}
                </TableCell>
              </TableRow>
              {isOpen && (
                <TableRow className="hover:bg-transparent">
                  <TableCell colSpan={4} className="bg-muted/30 whitespace-normal">
                    <div className="px-2 py-1 text-sm">
                      <p className="text-muted-foreground">
                        Ships to {order.address}
                      </p>
                      <p className="mt-1">Items: {order.items.join(", ")}</p>
                    </div>
                  </TableCell>
                </TableRow>
              )}
            </React.Fragment>
          )
        })}
      </TableBody>
    </Table>
  )
}
```
