# Table (/ui/components/react/table)



<Playground component="table" />

## Usage [#usage]

```tsx
import { Table, TableCaption, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@appica/ui-react/table'
```

```tsx
<Table>
  <TableCaption>Recent transactions</TableCaption>
  <TableHeader>
    <TableRow>
      <TableHead>Date</TableHead>
      <TableHead className="text-end">Amount</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell>Aug 30</TableCell>
      <TableCell className="text-end">+ $4,150.00</TableCell>
    </TableRow>
  </TableBody>
</Table>
```

`Table` is a thin, styled layer over the native table elements — it keeps the real `<table>`, `<thead>`, `<tbody>`, `<tr>`, `<th>`, and `<td>` semantics and just adds the surface, spacing, rounded corners, and optional striping/hover. All parts live under `@appica/ui-react/table`:

* **`Table`** — the `<table>` root. Owns `size`, `borderStyle`, and the `stripedRows` / `stripedColumns` / `hoverableRows` toggles.
* **`TableHeader`*&#x2A; / &#x2A;*`TableBody`** — `<thead>` / `<tbody>` wrappers. The header gets a tinted bar; the body holds the data rows.
* **`TableRow`** — a `<tr>`. Mark one `highlighted` to call it out.
* **`TableHead`** — a `<th>` header cell (start-aligned by default).
* **`TableCell`** — a `<td>` data cell.
* **`TableCaption`** — a `<caption>` that sits above or below the table via `position`.

The rounded corners are applied automatically to the right cells, so you don't manage them by hand. Per-cell alignment (e.g. right-aligned amounts) is just a `text-end` utility on the relevant `TableHead`/`TableCell`.

## Examples [#examples]

### Default [#default]

Header, body, a caption, and a right-aligned amount column. The outer border, header bar, and corner rounding come for free.

```tsx
import { Table, TableCaption, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@appica/ui-react/table'

const rows = [
  { date: 'Aug 30', description: 'Salary', amount: '+ $4,150.00' },
  { date: 'Aug 24', description: 'Netflix', amount: '- $20.00' },
  { date: 'Aug 17', description: 'Electricity', amount: '- $186.35' },
  { date: 'Aug 12', description: 'Grocery store', amount: '- $74.20' },
]

export default function TableDefault() {
  return (
    <Table>
      <TableCaption>Recent transactions</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead>Date</TableHead>
          <TableHead>Description</TableHead>
          <TableHead className="text-end">Amount</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {rows.map((row) => (
          <TableRow key={row.description}>
            <TableCell>{row.date}</TableCell>
            <TableCell className="text-foreground-strong font-medium">{row.description}</TableCell>
            <TableCell className="text-end tabular-nums">{row.amount}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

### Sizes [#sizes]

`size` scales the cell padding, corner radius, and text together — `sm`, `md` (default), or `lg`.

```tsx
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@appica/ui-react/table'

const sizes = ['sm', 'md', 'lg'] as const

const rows = [
  { name: 'Starter', seats: '1–3', price: '$0' },
  { name: 'Team', seats: 'Up to 20', price: '$12' },
]

export default function TableSizes() {
  return (
    <div className="flex w-full flex-col gap-8">
      {sizes.map((size) => (
        <div key={size} className="flex flex-col gap-2">
          <span className="text-foreground-muted text-xs font-medium">size=&quot;{size}&quot;</span>
          <Table size={size}>
            <TableHeader>
              <TableRow>
                <TableHead>Plan</TableHead>
                <TableHead>Seats</TableHead>
                <TableHead className="text-end">Per month</TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {rows.map((row) => (
                <TableRow key={row.name}>
                  <TableCell className="text-foreground-strong font-medium">{row.name}</TableCell>
                  <TableCell>{row.seats}</TableCell>
                  <TableCell className="text-end tabular-nums">{row.price}</TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </div>
      ))}
    </div>
  )
}
```

### Striped [#striped]

`stripedRows` tints alternating rows for easier horizontal scanning; `stripedColumns` does the same down the columns. Use whichever fits the data — or neither.

```tsx
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@appica/ui-react/table'

const rows = [
  { region: 'North America', q3: '$48.2k', q4: '$52.9k' },
  { region: 'Europe', q3: '$31.7k', q4: '$36.4k' },
  { region: 'Asia Pacific', q3: '$22.1k', q4: '$28.8k' },
  { region: 'Latin America', q3: '$9.4k', q4: '$11.2k' },
]

export default function TableStriped() {
  return (
    <div className="flex w-full flex-col gap-8">
      <div className="flex flex-col gap-2">
        <span className="text-foreground-muted text-xs font-medium">stripedRows</span>
        <Table stripedRows>
          <TableHeader>
            <TableRow>
              <TableHead>Region</TableHead>
              <TableHead className="text-end">Q3</TableHead>
              <TableHead className="text-end">Q4</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {rows.map((row) => (
              <TableRow key={row.region}>
                <TableCell className="text-foreground-strong font-medium">{row.region}</TableCell>
                <TableCell className="text-end tabular-nums">{row.q3}</TableCell>
                <TableCell className="text-end tabular-nums">{row.q4}</TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </div>

      <div className="flex flex-col gap-2">
        <span className="text-foreground-muted text-xs font-medium">stripedColumns</span>
        <Table stripedColumns>
          <TableHeader>
            <TableRow>
              <TableHead>Region</TableHead>
              <TableHead className="text-end">Q3</TableHead>
              <TableHead className="text-end">Q4</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {rows.map((row) => (
              <TableRow key={row.region}>
                <TableCell className="text-foreground-strong font-medium">{row.region}</TableCell>
                <TableCell className="text-end tabular-nums">{row.q3}</TableCell>
                <TableCell className="text-end tabular-nums">{row.q4}</TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </div>
    </div>
  )
}
```

### Hoverable rows [#hoverable-rows]

`hoverableRows` tints a row as the pointer passes over it — handy when each row is a target you'll click or act on.

```tsx
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@appica/ui-react/table'

const rows = [
  { file: 'annual-report.pdf', size: '2.4 MB', modified: 'Aug 30' },
  { file: 'budget-2025.xlsx', size: '845 KB', modified: 'Aug 28' },
  { file: 'logo-final.svg', size: '12 KB', modified: 'Aug 21' },
  { file: 'roadmap.key', size: '6.1 MB', modified: 'Aug 14' },
]

export default function TableHoverable() {
  return (
    <Table hoverableRows>
      <TableHeader>
        <TableRow>
          <TableHead>File</TableHead>
          <TableHead>Size</TableHead>
          <TableHead>Modified</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {rows.map((row) => (
          <TableRow key={row.file}>
            <TableCell className="text-foreground-strong font-medium">{row.file}</TableCell>
            <TableCell className="tabular-nums">{row.size}</TableCell>
            <TableCell>{row.modified}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

### Border styles [#border-styles]

`borderStyle` switches the cell separators: `solid` (default), `dashed`, or `none` for a borderless, airy look.

```tsx
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@appica/ui-react/table'

const styles = ['solid', 'dashed', 'none'] as const

const rows = [
  { name: 'Sarah Jenkins', role: 'Engineer' },
  { name: 'Daniel Reyes', role: 'Designer' },
  { name: 'Mateo Rossi', role: 'Researcher' },
]

export default function TableBorderStyles() {
  return (
    <div className="flex w-full flex-col gap-8">
      {styles.map((borderStyle) => (
        <div key={borderStyle} className="flex flex-col gap-2">
          <span className="text-foreground-muted text-xs font-medium">borderStyle=&quot;{borderStyle}&quot;</span>
          <Table borderStyle={borderStyle}>
            <TableHeader>
              <TableRow>
                <TableHead>Name</TableHead>
                <TableHead>Role</TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {rows.map((row) => (
                <TableRow key={row.name}>
                  <TableCell className="text-foreground-strong font-medium">{row.name}</TableCell>
                  <TableCell>{row.role}</TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </div>
      ))}
    </div>
  )
}
```

### Highlighted row [#highlighted-row]

Mark a `TableRow` as `highlighted` to single it out — a selected row, the current plan, or a flagged entry. It keeps its background through striping and hover.

```tsx
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@appica/ui-react/table'

const plans = [
  { name: 'Starter', seats: '1–3', price: '$0', current: false },
  { name: 'Team', seats: 'Up to 20', price: '$12', current: true },
  { name: 'Business', seats: 'Up to 100', price: '$29', current: false },
  { name: 'Enterprise', seats: 'Unlimited', price: 'Custom', current: false },
]

export default function TableHighlighted() {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Plan</TableHead>
          <TableHead>Seats</TableHead>
          <TableHead className="text-end">Per month</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {plans.map((plan) => (
          <TableRow key={plan.name} highlighted={plan.current}>
            <TableCell className="text-foreground-strong font-medium">{plan.name}</TableCell>
            <TableCell>{plan.seats}</TableCell>
            <TableCell className="text-end tabular-nums">{plan.price}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

### Rich content [#rich-content]

Cells take any content, so compose them with other components — an [`Avatar`](/ui/components/react/avatar) and stacked text in one cell, a [`Badge`](/ui/components/react/badge) for status in another — and pull the caption to the top with `position="top"`.

```tsx
import { Table, TableCaption, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@appica/ui-react/table'
import { Avatar, AvatarImage, AvatarFallback } from '@appica/ui-react/avatar'
import { Badge } from '@appica/ui-react/badge'

const members = [
  { src: '/avatars/01.jpg', name: 'Sarah Jenkins', email: 'sarah@appica.dev', role: 'Owner', status: 'Active' },
  { src: '/avatars/02.jpg', name: 'Daniel Reyes', email: 'daniel@appica.dev', role: 'Editor', status: 'Active' },
  { src: '/avatars/03.jpg', name: 'Mateo Rossi', email: 'mateo@appica.dev', role: 'Viewer', status: 'Invited' },
] as const

const statusVariant = {
  Active: 'success',
  Invited: 'warning',
} as const

export default function TableRichContent() {
  return (
    <Table hoverableRows>
      <TableCaption position="top" className="text-foreground-intense text-start font-medium">
        Workspace members
      </TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead>Member</TableHead>
          <TableHead>Role</TableHead>
          <TableHead className="text-end">Status</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {members.map((member) => (
          <TableRow key={member.email}>
            <TableCell>
              <div className="flex items-center gap-3">
                <Avatar size="sm">
                  <AvatarImage src={member.src} alt={member.name} />
                  <AvatarFallback>
                    {member.name
                      .split(' ')
                      .map((part) => part[0])
                      .join('')}
                  </AvatarFallback>
                </Avatar>
                <div className="flex flex-col">
                  <span className="text-foreground-emphasis font-medium">{member.name}</span>
                  <span className="text-xs">{member.email}</span>
                </div>
              </div>
            </TableCell>
            <TableCell>{member.role}</TableCell>
            <TableCell className="text-end">
              <Badge variant={statusVariant[member.status]} size="sm">
                {member.status}
              </Badge>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

### Scrollable [#scrollable]

For a long table, wrap it in a [`ScrollArea`](/ui/components/react/scroll-area) with a fixed height. Pin the header to the top of the viewport with `[&>thead>tr>th]:sticky [&>thead>tr>th]:top-0` so the columns stay labelled while the rows scroll.

```tsx
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@appica/ui-react/table'
import { ScrollArea } from '@appica/ui-react/scroll-area'

const rows = [
  { date: 'Aug 30', description: 'Salary', amount: '+ $4,150.00' },
  { date: 'Aug 28', description: 'Spotify', amount: '- $11.99' },
  { date: 'Aug 24', description: 'Netflix', amount: '- $20.00' },
  { date: 'Aug 22', description: 'Freelance invoice', amount: '+ $1,200.00' },
  { date: 'Aug 19', description: 'Coffee roasters', amount: '- $18.40' },
  { date: 'Aug 17', description: 'Electricity', amount: '- $186.35' },
  { date: 'Aug 14', description: 'Gym membership', amount: '- $39.00' },
  { date: 'Aug 12', description: 'Grocery store', amount: '- $74.20' },
  { date: 'Aug 09', description: 'Refund — returns', amount: '+ $52.10' },
  { date: 'Aug 06', description: 'Phone bill', amount: '- $45.00' },
  { date: 'Aug 03', description: 'Bookshop', amount: '- $28.75' },
  { date: 'Aug 01', description: 'Rent', amount: '- $1,650.00' },
]

export default function TableScrollable() {
  return (
    <ScrollArea className="border-border-muted h-75 w-full max-w-md rounded-lg border p-2">
      <Table className="border-0 p-0 [&>thead>tr>th]:sticky [&>thead>tr>th]:top-0">
        <TableHeader>
          <TableRow>
            <TableHead>Date</TableHead>
            <TableHead>Description</TableHead>
            <TableHead className="text-end">Amount</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((row) => (
            <TableRow key={row.description}>
              <TableCell>{row.date}</TableCell>
              <TableCell className="text-foreground-strong font-medium">{row.description}</TableCell>
              <TableCell className="text-end tabular-nums">{row.amount}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </ScrollArea>
  )
}
```

### Horizontal scroll [#horizontal-scroll]

When a table has more columns than will fit, wrap it in a horizontal `ScrollArea` and keep the cells from wrapping with `whitespace-nowrap`. The columns scroll sideways instead of cramping.

```tsx
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@appica/ui-react/table'
import { ScrollArea } from '@appica/ui-react/scroll-area'

const rows = [
  {
    name: 'Sarah Jenkins',
    email: 'sarah@appica.dev',
    role: 'Owner',
    team: 'Product',
    location: 'San Francisco',
    joined: 'Jan 2021',
  },
  {
    name: 'Daniel Reyes',
    email: 'daniel@appica.dev',
    role: 'Editor',
    team: 'Engineering',
    location: 'Austin',
    joined: 'Mar 2022',
  },
  {
    name: 'Mateo Rossi',
    email: 'mateo@appica.dev',
    role: 'Viewer',
    team: 'Design',
    location: 'Milan',
    joined: 'Jul 2022',
  },
  {
    name: 'Amara Okafor',
    email: 'amara@appica.dev',
    role: 'Editor',
    team: 'Marketing',
    location: 'Lagos',
    joined: 'Nov 2023',
  },
  {
    name: 'Noah Patel',
    email: 'noah@appica.dev',
    role: 'Viewer',
    team: 'Support',
    location: 'Toronto',
    joined: 'Feb 2024',
  },
]

export default function TableWide() {
  return (
    <ScrollArea orientation="horizontal" className="w-full max-w-md">
      <Table className="whitespace-nowrap">
        <TableHeader>
          <TableRow>
            <TableHead>Name</TableHead>
            <TableHead>Email</TableHead>
            <TableHead>Role</TableHead>
            <TableHead>Team</TableHead>
            <TableHead>Location</TableHead>
            <TableHead>Joined</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((row) => (
            <TableRow key={row.email}>
              <TableCell className="text-foreground-strong font-medium">{row.name}</TableCell>
              <TableCell>{row.email}</TableCell>
              <TableCell>{row.role}</TableCell>
              <TableCell>{row.team}</TableCell>
              <TableCell>{row.location}</TableCell>
              <TableCell>{row.joined}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </ScrollArea>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

Columns lay out from the right, the rounded corners mirror to the correct sides, and a `text-end` amount column moves to the left edge — all because the styling uses logical properties. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="table" />

## API reference [#api-reference]

`Table` and its parts are styled native table elements — they're not Base UI primitives. Each forwards `ref` and every remaining attribute to its underlying element, so standard table attributes (`colSpan`, `scope`, …) work as usual.

### Table [#table]

The `<table>` root. Owns the surface, sizing, and row/column treatments.

| Prop             | <ColMinWidth width="190">Type</ColMinWidth> | Default   | <ColMinWidth width="230">Description</ColMinWidth> |
| ---------------- | ------------------------------------------- | --------- | -------------------------------------------------- |
| `size`           | <Code>'sm' \| 'md' \| 'lg'</Code>           | `'md'`    | Cell padding, corner radius, and text scale.       |
| `borderStyle`    | <Code>'solid' \| 'dashed' \| 'none'</Code>  | `'solid'` | Style of the cell separators.                      |
| `stripedRows`    | `boolean`                                   | `false`   | Tint alternating rows.                             |
| `stripedColumns` | `boolean`                                   | `false`   | Tint alternating columns.                          |
| `hoverableRows`  | `boolean`                                   | `false`   | Tint a row on pointer hover.                       |
| `className`      | `string`                                    | —         | Extra classes, merged via `tailwind-merge`.        |

### TableCaption [#tablecaption]

A `<caption>` describing the table. Placed above or below.

| Prop        | <ColMinWidth width="190">Type</ColMinWidth> | Default    | <ColMinWidth width="230">Description</ColMinWidth> |
| ----------- | ------------------------------------------- | ---------- | -------------------------------------------------- |
| `position`  | <Code>'top' \| 'bottom'</Code>              | `'bottom'` | Render the caption above or below the table.       |
| `className` | `string`                                    | —          | Extra classes, merged via `tailwind-merge`.        |

### TableRow [#tablerow]

A `<tr>`. Use `highlighted` to call out a single row.

| Prop          | <ColMinWidth width="190">Type</ColMinWidth> | Default | <ColMinWidth width="230">Description</ColMinWidth>              |
| ------------- | ------------------------------------------- | ------- | --------------------------------------------------------------- |
| `highlighted` | `boolean`                                   | `false` | Apply the persistent highlight background (`data-highlighted`). |
| `className`   | `string`                                    | —       | Extra classes, merged via `tailwind-merge`.                     |

### TableHeader, TableBody, TableHead, TableCell [#tableheader-tablebody-tablehead-tablecell]

Thin wrappers over `<thead>`, `<tbody>`, `<th>`, and `<td>`. They take their native attributes plus `className` (merged via `tailwind-merge`). `TableHead` is start-aligned by default — add `text-end` (or `text-center`) for other alignments.

## Accessibility [#accessibility]

* The component renders real table semantics — `<table>`, `<thead>`, `<tbody>`, `<tr>`, `<th>`, `<td>` — so screen readers announce rows, columns, and headers natively.
* Use `TableHead` for header cells, and add `scope="col"` (or `scope="row"` for a row header) when the relationship isn't obvious, so assistive tech maps each data cell to its header.
* A `TableCaption` gives the table an accessible name; keep it even when it's visually subtle.
* `highlighted` is a visual cue only — if the highlighted row carries meaning (e.g. "current plan"), convey that in text as well, not by color alone.
* Row hover and the highlight are background-only changes that don't affect the reading order or content.
