# Table

A responsive table component.

```tsx
import { Badge } from "@/components/ui/badge"
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 TableDemo() {
  return (
    <Table>
      <TableCaption>A list of your recent invoices.</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead>Invoice</TableHead>
          <TableHead>Status</TableHead>
          <TableHead>Method</TableHead>
          <TableHead align="end">Amount</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {invoices.map((row) => (
          <TableRow key={row.invoice}>
            <TableHead>{row.invoice}</TableHead>
            <TableCell>
              <Badge variant={row.status === "Unpaid" ? "outline" : "secondary"}>
                {row.status}
              </Badge>
            </TableCell>
            <TableCell>{row.method}</TableCell>
            <TableCell align="end">{row.amount}</TableCell>
          </TableRow>
        ))}
      </TableBody>
      <TableFooter>
        <TableRow>
          <TableCell colSpan={3}>Total</TableCell>
          <TableCell align="end">$1,750.00</TableCell>
        </TableRow>
      </TableFooter>
    </Table>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation).

3. Copy and paste the following code into your project.

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

   import { createContext, use } from "react"
   import type { ComponentProps } from "react"
   import { useRender } from "@base-ui/react/use-render"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"

   import {
     color,
     duration,
     ease,
     leading,
     radius,
     shadow,
     space,
     text,
   } from "@/theme/tokens.stylex"

   type TableSection = "header" | "body" | "footer"

   const TableSectionContext = createContext<TableSection>("body")

   const styles = stylex.create({
     container: {
       "--carbon-table-radius": radius.xl,
       "--carbon-table-panel-radius": `max(0px, calc(var(--carbon-table-radius) - ${space.s1}))`,
       position: "relative",
       width: "100%",
       overflowX: "auto",
       padding: space.s1,
       borderRadius: "var(--carbon-table-radius)",
       backgroundColor: color.tray,
     },
     table: {
       width: "100%",
       borderCollapse: "separate",
       borderSpacing: 0,
       captionSide: "bottom",
       isolation: "isolate",
       color: color.foreground,
       fontSize: text.sm,
       lineHeight: leading.sm,
       fontVariantNumeric: "tabular-nums",
     },
     body: {
       position: "relative",
       "::before": {
         content: "''",
         position: "absolute",
         inset: 0,
         zIndex: -1,
         borderRadius: "var(--carbon-table-panel-radius)",
         backgroundColor: color.card,
         boxShadow: shadow.raised,
         pointerEvents: "none",
       },
     },
     footer: { fontWeight: 500 },
     bodyRow: {
       "--carbon-table-row-bg": { default: color.transparent, ":hover": color.itemHighlightBg },
       "--carbon-table-seam": { default: "1px", ":last-child": "0px" },
       "--carbon-table-corner-start": {
         default: "0px",
         ":first-child": "var(--carbon-table-panel-radius)",
       },
       "--carbon-table-corner-end": {
         default: "0px",
         ":last-child": "var(--carbon-table-panel-radius)",
       },
     },
     cell: {
       paddingInline: space.s3,
       paddingBlock: space.s2_5,
       textAlign: "start",
       verticalAlign: "middle",
       whiteSpace: "nowrap",
     },
     bodyCell: {
       backgroundColor: "var(--carbon-table-row-bg)",
       borderBlockEndWidth: "var(--carbon-table-seam)",
       borderBlockEndStyle: "solid",
       borderBlockEndColor: color.border,
       borderStartStartRadius: { default: null, ":first-child": "var(--carbon-table-corner-start)" },
       borderStartEndRadius: { default: null, ":last-child": "var(--carbon-table-corner-start)" },
       borderEndStartRadius: { default: null, ":first-child": "var(--carbon-table-corner-end)" },
       borderEndEndRadius: { default: null, ":last-child": "var(--carbon-table-corner-end)" },
       transitionProperty: "background-color",
       transitionDuration: duration.color,
       transitionTimingFunction: ease.standard,
     },
     head: {
       paddingBlock: space.s2,
       fontSize: text.xs,
       lineHeight: leading.xs,
       fontWeight: 500,
       color: color.mutedForeground,
     },
     rowHead: { fontWeight: 500 },
     caption: {
       paddingInline: space.s3,
       paddingBlockStart: space.s3,
       paddingBlockEnd: space.s2,
       color: color.mutedForeground,
       fontSize: text.xs,
       lineHeight: leading.xs,
       textAlign: "start",
     },
   })

   const aligns = stylex.create({
     start: { textAlign: "start" },
     center: { textAlign: "center" },
     end: { textAlign: "end" },
   })

   const tableStyles = {
     container: styles.container,
     table: styles.table,
     body: styles.body,
     footer: styles.footer,
     bodyRow: styles.bodyRow,
     cell: styles.cell,
     bodyCell: styles.bodyCell,
     head: styles.head,
     caption: styles.caption,
     aligns,
   }

   type TableAlign = keyof typeof aligns

   type Overrides = { xstyle?: StyleXStyles; className?: string }

   type TableProps = useRender.ComponentProps<"table"> &
     Overrides & {
       containerProps?: ComponentProps<"div"> & Overrides
     }

   function Table({ render, ref, style, xstyle, className, containerProps, ...props }: TableProps) {
     const {
       xstyle: containerXstyle,
       className: containerClassName,
       style: containerStyle,
       ...containerRest
     } = containerProps ?? {}
     const containerSx = stylex.props(styles.container, containerXstyle)
     const sx = stylex.props(styles.table, xstyle)
     const table = useRender({
       defaultTagName: "table",
       render,
       ref,
       props: {
         "data-slot": "table",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
     return (
       <div
         data-slot="table-container"
         {...containerSx}
         {...containerRest}
         className={clsx(containerSx.className, containerClassName)}
         style={{ ...containerSx.style, ...containerStyle }}
       >
         {table}
       </div>
     )
   }

   type SectionProps<T extends "thead" | "tbody" | "tfoot"> = useRender.ComponentProps<T> & Overrides

   function TableHeader({ render, ref, style, xstyle, className, ...props }: SectionProps<"thead">) {
     const sx = stylex.props(xstyle)
     const element = useRender({
       defaultTagName: "thead",
       render,
       ref,
       props: {
         "data-slot": "table-header",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
     return <TableSectionContext value="header">{element}</TableSectionContext>
   }

   function TableBody({ render, ref, style, xstyle, className, ...props }: SectionProps<"tbody">) {
     const sx = stylex.props(styles.body, xstyle)
     const element = useRender({
       defaultTagName: "tbody",
       render,
       ref,
       props: {
         "data-slot": "table-body",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
     return <TableSectionContext value="body">{element}</TableSectionContext>
   }

   function TableFooter({ render, ref, style, xstyle, className, ...props }: SectionProps<"tfoot">) {
     const sx = stylex.props(styles.footer, xstyle)
     const element = useRender({
       defaultTagName: "tfoot",
       render,
       ref,
       props: {
         "data-slot": "table-footer",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
     return <TableSectionContext value="footer">{element}</TableSectionContext>
   }

   function TableRow({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"tr"> & Overrides) {
     const section = use(TableSectionContext)
     const sx = stylex.props(section === "body" && styles.bodyRow, xstyle)
     return useRender({
       defaultTagName: "tr",
       render,
       ref,
       props: {
         "data-slot": "table-row",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   type CellProps<T extends "th" | "td"> = Omit<useRender.ComponentProps<T>, "align"> &
     Overrides & {
       align?: TableAlign
     }

   function TableHead({
     align = "start",
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: CellProps<"th">) {
     const section = use(TableSectionContext)
     const inBody = section === "body"
     const sx = stylex.props(
       styles.cell,
       inBody ? [styles.bodyCell, styles.rowHead] : styles.head,
       aligns[align],
       xstyle,
     )
     return useRender({
       defaultTagName: "th",
       render,
       ref,
       props: {
         "data-slot": "table-head",
         scope: inBody ? "row" : "col",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function TableCell({
     align = "start",
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: CellProps<"td">) {
     const section = use(TableSectionContext)
     const sx = stylex.props(styles.cell, section === "body" && styles.bodyCell, aligns[align], xstyle)
     return useRender({
       defaultTagName: "td",
       render,
       ref,
       props: {
         "data-slot": "table-cell",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function TableCaption({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"caption"> & Overrides) {
     const sx = stylex.props(styles.caption, xstyle)
     return useRender({
       defaultTagName: "caption",
       render,
       ref,
       props: {
         "data-slot": "table-caption",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

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

4. Update the import paths to match your project setup.

## Usage

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

```tsx
<Table>
  <TableCaption>A list of your recent invoices.</TableCaption>
  <TableHeader>
    <TableRow>
      <TableHead>Invoice</TableHead>
      <TableHead>Status</TableHead>
      <TableHead align="end">Amount</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell>INV001</TableCell>
      <TableCell>Paid</TableCell>
      <TableCell align="end">$250.00</TableCell>
    </TableRow>
  </TableBody>
</Table>
```

## Composition

```text
Table
├── TableCaption
├── TableHeader
│   └── TableRow
│       └── TableHead
├── TableBody
│   └── TableRow
│       ├── TableHead
│       └── TableCell
└── TableFooter
    └── TableRow
        └── TableCell
```

The table is drawn on a tray. `TableBody` is a raised panel inside it, and
`TableHeader`, `TableFooter` and `TableCaption` stay on the tray around the
panel. Body rows highlight on hover, and a hairline divides them.

## Alignment

Set `align="end"` on the heading and the cells of a column of figures. The
table uses tabular figures, so the digits line up.

## Actions

Put a menu in the last column. A `TableHead` in a body row heads that row, as
`INV001` does in the example above.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { HStack, VStack } from "@/components/ui/stack"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { color, leading, text } from "@/theme/tokens.stylex"
import { MoreHorizontal } from "lucide-react"

const members = [
  { name: "Olivia Martin", email: "olivia@example.com", initials: "OM", role: "Owner" },
  { name: "Jackson Lee", email: "jackson@example.com", initials: "JL", role: "Admin" },
  { name: "Isabella Nguyen", email: "isabella@example.com", initials: "IN", role: "Member" },
]

const styles = stylex.create({
  name: { fontWeight: 500 },
  email: { color: color.mutedForeground, fontSize: text.xs, lineHeight: leading.xs },
  actions: { width: "1%" },
})

export default function TableActions() {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Member</TableHead>
          <TableHead>Role</TableHead>
          <TableHead aria-label="Actions" xstyle={styles.actions} />
        </TableRow>
      </TableHeader>
      <TableBody>
        {members.map((member) => (
          <TableRow key={member.email}>
            <TableCell>
              <HStack gap={3} align="center">
                <Avatar>
                  <AvatarFallback>{member.initials}</AvatarFallback>
                </Avatar>
                <VStack>
                  <span {...stylex.props(styles.name)}>{member.name}</span>
                  <span {...stylex.props(styles.email)}>{member.email}</span>
                </VStack>
              </HStack>
            </TableCell>
            <TableCell>{member.role}</TableCell>
            <TableCell align="end">
              <DropdownMenu>
                <DropdownMenuTrigger
                  render={<Button variant="ghost" size="icon-sm" />}
                  aria-label={`Actions for ${member.name}`}
                >
                  <MoreHorizontal />
                </DropdownMenuTrigger>
                <DropdownMenuContent align="end">
                  <DropdownMenuItem>Change role</DropdownMenuItem>
                  <DropdownMenuItem>Resend invite</DropdownMenuItem>
                  <DropdownMenuSeparator />
                  <DropdownMenuItem variant="destructive">Remove</DropdownMenuItem>
                </DropdownMenuContent>
              </DropdownMenu>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

## API reference

Pass `render` to replace a part's element or compose it with another component.

Every part renders through Base UI's
[`useRender`](https://base-ui.com/react/utils/use-render), and takes `xstyle`,
`className` and inline `style` alongside every attribute of its element.

### Table

Renders a `table` inside a `div` that draws the tray and scrolls sideways when
the columns don't fit. `xstyle`, `className` and `render` go to the `table`.

| Prop             | Type                                                                    | Default     |
| ---------------- | ----------------------------------------------------------------------- | ----------- |
| `containerProps` | `ComponentProps<"div"> & { xstyle?: StyleXStyles; className?: string }` | —           |
| `render`         | `ReactElement \| ((props, state) => ReactElement)`                      | `<table />` |
| `xstyle`         | `StyleXStyles`                                                          | —           |
| `className`      | `string`                                                                | —           |

### TableHeader, TableBody, TableFooter

Render `thead`, `tbody` and `tfoot`. Each tells its rows which section they are
in, so only body rows are raised, divided and highlighted. `TableBody` draws
the panel behind itself. The footer's text is weight 500.

### TableRow

Renders a `tr`. In the body it highlights on hover and draws a hairline below
itself. The last row has no hairline, because the panel's edge is below it.

### TableHead

Renders a `th`. In the header it is a column label in small muted type with
`scope="col"`. In a body row it heads that row at weight 500 with
`scope="row"`. Pass `scope` to override either.

| Prop    | Type                           | Default   |
| ------- | ------------------------------ | --------- |
| `align` | `"start" \| "center" \| "end"` | `"start"` |

### TableCell

Renders a `td`. Cells don't wrap. To wrap one, pass `xstyle`.

| Prop    | Type                           | Default   |
| ------- | ------------------------------ | --------- |
| `align` | `"start" \| "center" \| "end"` | `"start"` |

### TableCaption

Renders a `caption` under the table, on the tray.

### tableStyles

The style maps behind the component. Spread them onto any element.

| Key         | What it is                                                  |
| ----------- | ----------------------------------------------------------- |
| `container` | The tray and horizontal scroller                            |
| `table`     | The table: separate borders, type, tabular figures          |
| `body`      | The raised panel behind the body                            |
| `footer`    | The footer's weight                                         |
| `bodyRow`   | The variables a body row sets for its cells                 |
| `cell`      | A cell's padding and alignment                              |
| `bodyCell`  | A body cell's fill, hairline and corners, read from the row |
| `head`      | A column label's type                                       |
| `caption`   | The caption                                                 |
| `aligns`    | `start`, `center` and `end`                                 |

### Data attributes

| Attribute                     | On          | Effect                            |
| ----------------------------- | ----------- | --------------------------------- |
| `data-slot="table-container"` | the tray    | Targets the tray from a parent    |
| `data-slot="table"`           | the table   | Targets the table from a parent   |
| `data-slot="table-header"`    | the header  | Targets the header from a parent  |
| `data-slot="table-body"`      | the body    | Targets the body from a parent    |
| `data-slot="table-footer"`    | the footer  | Targets the footer from a parent  |
| `data-slot="table-row"`       | a row       | Targets a row from a parent       |
| `data-slot="table-head"`      | a heading   | Targets a heading from a parent   |
| `data-slot="table-cell"`      | a cell      | Targets a cell from a parent      |
| `data-slot="table-caption"`   | the caption | Targets the caption from a parent |

### CSS variables

| Variable                      | On       | Effect                                                  |
| ----------------------------- | -------- | ------------------------------------------------------- |
| `--carbon-table-radius`       | the tray | The tray's corner radius, 12px. The panel's is 4px less |
| `--carbon-table-panel-radius` | the tray | The panel's corner, derived from the tray's             |
