# Table

> A responsive table with a surface style, wrapping or compact cells, sticky headers, pinned columns and scroll hints.

Docs: https://hextaui.com/docs/table
Markdown: https://hextaui.com/docs/table.md

```tsx title="components/examples/table/demo.tsx"
import { Badge, BadgeDot } from "@/components/ui/badge"
import {
  Table,
  TableBody,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const invoices = [
  { id: "INV-0418", client: "Northwind", status: "Paid", amount: 2400 },
  { id: "INV-0419", client: "Globex", status: "Pending", amount: 860 },
  { id: "INV-0420", client: "Initech", status: "Paid", amount: 1320.5 },
  { id: "INV-0421", client: "Umbrella", status: "Overdue", amount: 415 },
]

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

const tones = {
  Paid: "success",
  Pending: "info",
  Overdue: "destructive",
} as const

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

  return (
    <Table variant="surface" className="min-w-md">
      <TableHeader>
        <TableRow>
          <TableHead>Invoice</TableHead>
          <TableHead>Client</TableHead>
          <TableHead>Status</TableHead>
          <TableHead align="end">Amount</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {invoices.map((invoice) => (
          <TableRow key={invoice.id}>
            <TableHead scope="row">{invoice.id}</TableHead>
            <TableCell>{invoice.client}</TableCell>
            <TableCell>
              <Badge variant={tones[invoice.status as keyof typeof tones]}>
                <BadgeDot />
                {invoice.status}
              </Badge>
            </TableCell>
            <TableCell align="end">{currency.format(invoice.amount)}</TableCell>
          </TableRow>
        ))}
      </TableBody>
      <TableFooter>
        <TableRow>
          <TableCell colSpan={3}>Total</TableCell>
          <TableCell align="end">{currency.format(total)}</TableCell>
        </TableRow>
      </TableFooter>
    </Table>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/table.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add class-variance-authority cn
```

Copy and paste the following code into your project.

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

import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

function mergeRefs<T>(
  ...refs: (React.Ref<T> | ((node: T | null) => void) | undefined)[]
) {
  return (node: T | null) => {
    for (const ref of refs) {
      if (typeof ref === "function") {
        ref(node)
      } else if (ref) {
        ref.current = node
      }
    }
  }
}

function useScrollEdges(container: HTMLDivElement | null) {
  React.useLayoutEffect(() => {
    if (!container) {
      return
    }
    const update = () => {
      const offset = Math.abs(container.scrollLeft)
      const max = container.scrollWidth - container.clientWidth
      const overflowing =
        max > 0.5 || container.scrollHeight - container.clientHeight > 0.5
      container.toggleAttribute("data-scrolled-start", offset > 0.5)
      container.toggleAttribute("data-scrolled-end", offset < max - 0.5)
      container.toggleAttribute("data-scrolled-top", container.scrollTop > 0.5)
      container.toggleAttribute("data-overflowing", overflowing)
      if (overflowing) {
        container.tabIndex = 0
      } else {
        container.removeAttribute("tabindex")
      }
    }
    update()
    container.addEventListener("scroll", update, { passive: true })
    const observer =
      typeof ResizeObserver === "function" ? new ResizeObserver(update) : null
    observer?.observe(container)
    const table = container.firstElementChild
    if (table) {
      observer?.observe(table)
    }
    return () => {
      container.removeEventListener("scroll", update)
      observer?.disconnect()
    }
  }, [container])
}

const tableVariants = cva(
  "group/table relative w-full min-w-0 [--table-bg:var(--background)] [--table-hover:color-mix(in_oklab,var(--muted)_60%,var(--table-bg))] in-data-[slot=card]:[--table-bg:var(--card)] in-data-[slot=popover-content]:[--table-bg:var(--popover)]",
  {
    variants: {
      variant: {
        default: "",
        surface:
          "isolate overflow-clip rounded-xl bg-(--table-bg) ring-(length:--hairline) ring-border [--table-bg:var(--card)] [--table-head-bg:color-mix(in_oklab,var(--muted)_85%,var(--table-bg))] in-data-[slot=card]:[--table-bg:var(--card)] forced-colors:border",
      },
      size: {
        sm: "[--table-cell-px:--spacing(2.5)] [--table-cell-py:--spacing(1.5)] [--table-head-h:--spacing(8)]",
        default:
          "[--table-cell-px:--spacing(3)] [--table-cell-py:--spacing(2.5)] [--table-head-h:--spacing(10)]",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

type TableVariant = NonNullable<VariantProps<typeof tableVariants>["variant"]>
type TableSize = NonNullable<VariantProps<typeof tableVariants>["size"]>

type TableProps = React.ComponentProps<"table"> & {
  variant?: TableVariant
  size?: TableSize
  wrap?: boolean
  scrollFade?: boolean
  stickyHeader?: boolean
  containerClassName?: string
  containerRef?: React.Ref<HTMLDivElement>
}

function Table({
  className,
  variant = "default",
  size = "default",
  wrap = false,
  scrollFade = true,
  stickyHeader = false,
  containerClassName,
  containerRef,
  ...props
}: TableProps) {
  const [container, setContainer] = React.useState<HTMLDivElement | null>(null)
  useScrollEdges(container)

  const setRef = React.useMemo(
    () => mergeRefs(setContainer, containerRef),
    [containerRef]
  )

  return (
    <div
      data-slot="table-frame"
      data-variant={variant}
      data-size={size}
      data-wrap={wrap ? "" : undefined}
      className={tableVariants({ variant, size })}
    >
      <div
        ref={setRef}
        data-slot="table-container"
        data-sticky-header={stickyHeader ? "" : undefined}
        className={cn(
          "relative w-full overflow-x-auto overscroll-x-none rounded-[inherit] outline-none [--fade-end:0px] [--fade-start:0px] focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden focus-visible:ring-inset data-sticky-header:overflow-y-auto",
          scrollFade &&
            "not-has-[[data-pinned]]:mask-[linear-gradient(to_right,transparent,#000_var(--fade-start),#000_calc(100%-var(--fade-end)),transparent)] data-scrolled-end:[--fade-end:--spacing(8)] data-scrolled-start:[--fade-start:--spacing(8)] rtl:not-has-[[data-pinned]]:mask-[linear-gradient(to_left,transparent,#000_var(--fade-start),#000_calc(100%-var(--fade-end)),transparent)]",
          containerClassName
        )}
      >
        <table
          data-slot="table"
          className={cn(
            "w-full caption-bottom border-separate border-spacing-0 text-sm",
            className
          )}
          {...props}
        />
      </div>
    </div>
  )
}

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

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

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

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

const cellBase =
  "border-b align-middle whitespace-nowrap group-data-wrap/table:align-top group-data-wrap/table:whitespace-normal group-data-wrap/table:text-pretty group-data-wrap/table:wrap-break-word data-[align=center]:text-center data-[align=end]:text-end data-[align=end]:tabular-nums data-pinned:sticky data-pinned:z-1 data-pinned:has-[:focus-visible]:z-4 data-pinned:has-[[data-slot=checkbox]]:z-2 data-pinned:bg-(--table-bg) group-hover/table-row:data-pinned:bg-(--table-hover) group-data-[state=selected]/table-row:data-pinned:bg-muted data-[pinned=start]:start-(--pin-offset) data-[pinned=end]:end-(--pin-offset) data-pinned:[--pin-offset:0px] data-pinned-edge:after:pointer-events-none data-pinned-edge:after:absolute data-pinned-edge:after:inset-y-0 data-pinned-edge:after:hidden data-pinned-edge:after:w-4 data-[pinned=start]:data-pinned-edge:after:-end-4 data-[pinned=start]:data-pinned-edge:after:bg-linear-to-r data-[pinned=start]:data-pinned-edge:after:from-foreground/6 data-[pinned=start]:data-pinned-edge:after:to-transparent rtl:data-[pinned=start]:data-pinned-edge:after:bg-linear-to-l data-[pinned=end]:data-pinned-edge:after:-start-4 data-[pinned=end]:data-pinned-edge:after:bg-linear-to-l data-[pinned=end]:data-pinned-edge:after:from-foreground/6 data-[pinned=end]:data-pinned-edge:after:to-transparent rtl:data-[pinned=end]:data-pinned-edge:after:bg-linear-to-r in-data-scrolled-start:data-[pinned=start]:data-pinned-edge:after:block in-data-scrolled-end:data-[pinned=end]:data-pinned-edge:after:block [&:has([role=checkbox])]:w-px [&>[role=checkbox]]:flex group-data-[variant=surface]/table:first:ps-4 group-data-[variant=surface]/table:last:pe-4"

type TableCellExtraProps = {
  align?: "start" | "center" | "end"
  pinned?: "start" | "end"
  pinnedEdge?: boolean
}

function TableHead({
  className,
  align,
  pinned,
  pinnedEdge,
  ...props
}: Omit<React.ComponentProps<"th">, "align"> & TableCellExtraProps) {
  return (
    <th
      data-slot="table-head"
      data-align={align}
      data-pinned={pinned}
      data-pinned-edge={pinnedEdge ? "" : undefined}
      className={cn(
        cellBase,
        "h-(--table-head-h) bg-(--table-head-bg,var(--table-bg)) px-(--table-cell-px) text-start font-medium text-muted-foreground group-data-wrap/table:align-middle in-data-scrolled-top:shadow-[inset_0_calc(var(--hairline)*-1)_0_var(--border)] in-data-sticky-header:sticky in-data-sticky-header:top-0 in-data-sticky-header:z-2 in-[tbody]:h-auto in-[tbody]:bg-transparent in-[tbody]:py-(--table-cell-py) in-[tbody]:text-foreground in-[tbody]:shadow-none in-[tbody]:group-data-wrap/table:align-top data-pinned:bg-(--table-head-bg,var(--table-bg)) in-data-sticky-header:data-pinned:z-3 in-[tbody]:data-pinned:bg-(--table-bg) in-[tbody]:group-hover/table-row:data-pinned:bg-(--table-hover) in-[tbody]:group-data-[state=selected]/table-row:data-pinned:bg-muted [&:has([role=checkbox])]:pe-0",
        className
      )}
      {...props}
    />
  )
}

function TableCell({
  className,
  align,
  pinned,
  pinnedEdge,
  ...props
}: Omit<React.ComponentProps<"td">, "align"> & TableCellExtraProps) {
  return (
    <td
      data-slot="table-cell"
      data-align={align}
      data-pinned={pinned}
      data-pinned-edge={pinnedEdge ? "" : undefined}
      className={cn(
        cellBase,
        "px-(--table-cell-px) py-(--table-cell-py) [&:has([role=checkbox])]:pe-0",
        className
      )}
      {...props}
    />
  )
}

function TableCaption({
  className,
  ...props
}: React.ComponentProps<"caption">) {
  return (
    <caption
      data-slot="table-caption"
      className={cn(
        "mt-4 text-sm text-muted-foreground group-data-[variant=surface]/table:mt-0 group-data-[variant=surface]/table:border-t group-data-[variant=surface]/table:px-4 group-data-[variant=surface]/table:py-2.5 group-data-[variant=surface]/table:text-start",
        className
      )}
      {...props}
    />
  )
}

export {
  Table,
  TableHeader,
  TableBody,
  TableFooter,
  TableHead,
  TableRow,
  TableCell,
  TableCaption,
  tableVariants,
}
export type { TableProps, TableSize, TableVariant }
```

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 variant="surface">
  <TableHeader>
    <TableRow>
      <TableHead>Invoice</TableHead>
      <TableHead align="end">Amount</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableHead scope="row">INV-0418</TableHead>
      <TableCell align="end">$2,400.00</TableCell>
    </TableRow>
  </TableBody>
</Table>
```

Use `<TableHead scope="row">` for the cell that names each row. It reads as the row's header to screen readers and gets a stronger text color. For sorting, filtering and selection, build on `<DataTable />`.

## Composition

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

## Examples

### Default

The default variant has no surface, just hairlines between rows, for tables that sit inside a card or a page section. `<TableCaption />` describes the table below it.

```tsx title="components/examples/table/plain.tsx"
import {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const releases = [
  { version: "2.4.0", date: "Oct 2, 2026", changes: 18 },
  { version: "2.3.1", date: "Sep 21, 2026", changes: 4 },
  { version: "2.3.0", date: "Sep 12, 2026", changes: 23 },
]

export function TablePlain() {
  return (
    <Table>
      <TableCaption>Recent releases.</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead>Version</TableHead>
          <TableHead>Released</TableHead>
          <TableHead align="end">Changes</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {releases.map((release) => (
          <TableRow key={release.version}>
            <TableHead scope="row">{release.version}</TableHead>
            <TableCell>{release.date}</TableCell>
            <TableCell align="end">{release.changes}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

### Compact

`size="sm"` tightens row height and padding for dense data.

```tsx title="components/examples/table/sizes.tsx"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const rows = [
  { region: "us-east-1", latency: "42 ms", uptime: "99.99%" },
  { region: "eu-west-2", latency: "38 ms", uptime: "99.98%" },
  { region: "ap-south-1", latency: "71 ms", uptime: "99.95%" },
]

export function TableSizes() {
  return (
    <Table variant="surface" size="sm">
      <TableHeader>
        <TableRow>
          <TableHead>Region</TableHead>
          <TableHead align="end">Latency</TableHead>
          <TableHead align="end">Uptime</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {rows.map((row) => (
          <TableRow key={row.region}>
            <TableCell>
              <span className="font-mono text-xs">{row.region}</span>
            </TableCell>
            <TableCell align="end">{row.latency}</TableCell>
            <TableCell align="end">{row.uptime}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

### Wrapping text

Cells stay on one line by default, which suits data. `wrap` lets them wrap and aligns rows to the top, for prose like glossaries or API references. Pair it with `table-fixed` and column widths.

```tsx title="components/examples/table/wrap.tsx"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const terms = [
  {
    term: "Concentric radius",
    meaning:
      "An inner corner radius equal to the outer radius minus the gap between the two edges, so nested shapes look like they belong together.",
  },
  {
    term: "Hairline",
    meaning:
      "The thinnest line the screen can draw: 1px on standard displays and two thirds of a pixel on high-density ones.",
  },
]

export function TableWrap() {
  return (
    <Table variant="surface" wrap className="table-fixed">
      <TableHeader>
        <TableRow>
          <TableHead className="w-1/3">Term</TableHead>
          <TableHead>Meaning</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {terms.map((row) => (
          <TableRow key={row.term}>
            <TableHead scope="row">{row.term}</TableHead>
            <TableCell>
              <span className="text-muted-foreground">{row.meaning}</span>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

### Wide tables

Tables wider than their container scroll sideways. Soft fades show there's more on either side, and the scroll area becomes focusable so keyboard users can scroll it with the arrow keys. Turn the fades off with `scrollFade={false}`.

```tsx title="components/examples/table/scroll.tsx"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const months = [
  "Jan",
  "Feb",
  "Mar",
  "Apr",
  "May",
  "Jun",
  "Jul",
  "Aug",
  "Sep",
  "Oct",
  "Nov",
  "Dec",
]

const series = [
  {
    name: "Visitors",
    values: [
      812, 904, 1021, 990, 1203, 1340, 1288, 1410, 1502, 1622, 1590, 1711,
    ],
  },
  {
    name: "Sign-ups",
    values: [41, 52, 66, 59, 71, 83, 80, 92, 97, 110, 104, 121],
  },
]

export function TableScroll() {
  return (
    <Table variant="surface" className="w-max min-w-full">
      <TableHeader>
        <TableRow>
          <TableHead>Metric</TableHead>
          {months.map((month) => (
            <TableHead key={month} align="end">
              {month}
            </TableHead>
          ))}
        </TableRow>
      </TableHeader>
      <TableBody>
        {series.map((row) => (
          <TableRow key={row.name}>
            <TableHead scope="row">{row.name}</TableHead>
            {row.values.map((value, index) => (
              <TableCell key={months[index]} align="end">
                {value.toLocaleString("en-US")}
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

### Pinned column

`pinned="start"` keeps the first column in view while the rest scrolls, and `pinnedEdge` shows a shadow once content slides under it.

```tsx title="components/examples/table/pinned.tsx"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const quarters = [
  "Q1 2025",
  "Q2 2025",
  "Q3 2025",
  "Q4 2025",
  "Q1 2026",
  "Q2 2026",
  "Q3 2026",
]

const teams = [
  { name: "Design systems", values: [12, 14, 15, 18, 21, 22, 24] },
  { name: "Platform", values: [31, 30, 33, 35, 34, 38, 40] },
  { name: "Growth", values: [8, 9, 11, 10, 12, 15, 16] },
]

export function TablePinned() {
  return (
    <Table variant="surface" className="w-max min-w-full">
      <TableHeader>
        <TableRow>
          <TableHead pinned="start" pinnedEdge>
            Team
          </TableHead>
          {quarters.map((quarter) => (
            <TableHead key={quarter} align="end">
              {quarter}
            </TableHead>
          ))}
        </TableRow>
      </TableHeader>
      <TableBody>
        {teams.map((team) => (
          <TableRow key={team.name}>
            <TableHead scope="row" pinned="start" pinnedEdge>
              {team.name}
            </TableHead>
            {team.values.map((value, index) => (
              <TableCell key={quarters[index]} align="end">
                {value}
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

### Sticky header

`stickyHeader` keeps the header in view in a height-limited table, and adds a hairline under it once rows scroll beneath.

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

const events = Array.from({ length: 24 }, (_, index) => ({
  id: `evt_${(1024 + index).toString(16)}`,
  type: ["deploy", "rollback", "scale", "restart"][index % 4],
  minutes: index * 7 + 2,
}))

export function TableSticky() {
  return (
    <Table variant="surface" stickyHeader containerClassName="max-h-72">
      <TableHeader>
        <TableRow>
          <TableHead>Event</TableHead>
          <TableHead>Type</TableHead>
          <TableHead align="end">When</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {events.map((event) => (
          <TableRow key={event.id}>
            <TableCell>
              <span className="font-mono text-xs">{event.id}</span>
            </TableCell>
            <TableCell>{event.type}</TableCell>
            <TableCell align="end">{event.minutes} min ago</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

### Right to left

Alignment, pinned columns and scroll fades follow the reading direction.

```tsx title="components/examples/table/rtl.tsx"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const rows = [
  { name: "التصميم", members: 8, budget: "١٢٬٠٠٠" },
  { name: "الهندسة", members: 21, budget: "٤٨٬٥٠٠" },
]

export function TableRtl() {
  return (
    <div dir="rtl" className="w-full">
      <Table variant="surface">
        <TableHeader>
          <TableRow>
            <TableHead>الفريق</TableHead>
            <TableHead align="end">الأعضاء</TableHead>
            <TableHead align="end">الميزانية</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((row) => (
            <TableRow key={row.name}>
              <TableHead scope="row">{row.name}</TableHead>
              <TableCell align="end">{row.members}</TableCell>
              <TableCell align="end">{row.budget}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  )
}
```

## Keyboard

| Key | Action |
| --- | --- |
| `Tab` | Focuses the scroll area when the table overflows, then the links and buttons inside it. |
| `←` `→` | Scrolls a focused, overflowing table sideways. |
| `↑` `↓` | Scrolls a focused table with a sticky header up and down. |

## Accessibility

- The parts render native table elements, so screen readers announce rows, columns and headers without extra roles.
- Mark the cell that names a row with `<TableHead scope="row">` so each value is read with its row and column.
- The scroll area only joins the tab order while it overflows, so tables that fit don't add an extra stop.

## API reference

Every part renders its native element and accepts its attributes.

### Table

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "surface"` | `"default"` | surface adds a rounded hairline frame and a muted header band. |
| `size` | `"sm" \| "default"` | `"default"` |  |
| `wrap` | `boolean` | `false` | Let cell text wrap and align rows to the top. |
| `scrollFade` | `boolean` | `true` | Fade the edges that have more content to scroll to. Skipped when columns are pinned. |
| `stickyHeader` | `boolean` | `false` | Keep the header in view. Give the container a max height through containerClassName. |
| `containerClassName` | `string` | – | Classes for the scroll container. |
| `containerRef` | `Ref<HTMLDivElement>` | – | Ref to the scroll container. |

| Attribute | Description |
| --- | --- |
| `data-slot="table-frame"` | The outer frame, with data-variant, data-size and data-wrap. |
| `data-slot="table-container"` | The scroll container. |
| `data-overflowing` | Present on the container while its content overflows. It's focusable then. |
| `data-scrolled-start / data-scrolled-end` | Present while there's content hidden before or after the visible area. |
| `data-scrolled-top` | Present once a sticky-header table has scrolled. |
| `--table-bg` | Row background. Follows a surrounding card or popover. |
| `--table-head-bg` | Header band background in the surface variant. |
| `--table-cell-px / --table-cell-py` | Cell padding. Set by size. |

### TableHead

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `scope` | `"col" \| "row"` | – | Use row for the cell that names a row. Inside the body it gets cell padding and foreground text. |
| `align` | `"start" \| "center" \| "end"` | `"start"` | end also switches to tabular numbers so digits line up. |
| `pinned` | `"start" \| "end"` | – | Keep the column in view while the table scrolls sideways. Set the same value on its header and every cell. |
| `pinnedEdge` | `boolean` | `false` | Draw a soft shadow on the inner edge of the last pinned column while content is hidden behind it. |

| Attribute | Description |
| --- | --- |
| `data-slot="table-head"` | Target header cells in CSS. |
| `data-align` | The alignment. |
| `data-pinned` | The pinned side. |

### TableCell

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"start" \| "center" \| "end"` | `"start"` | end also switches to tabular numbers so digits line up. |
| `pinned` | `"start" \| "end"` | – | Keep the column in view while the table scrolls sideways. Set the same value on its header and every cell. |
| `pinnedEdge` | `boolean` | `false` | Draw a soft shadow on the inner edge of the last pinned column while content is hidden behind it. |

| Attribute | Description |
| --- | --- |
| `data-slot="table-cell"` | Target cells in CSS. |
| `data-align` | The alignment. |
| `data-pinned` | The pinned side. |

### TableRow

| Attribute | Description |
| --- | --- |
| `data-slot="table-row"` | Target rows in CSS. |
| `data-state="selected"` | Set it to highlight a selected row. |

### TableHeader, TableBody, TableFooter and TableCaption

| Attribute | Description |
| --- | --- |
| `data-slot="table-header"` | The thead. |
| `data-slot="table-body"` | The tbody. |
| `data-slot="table-footer"` | The tfoot, on the header band color. |
| `data-slot="table-caption"` | Below the table. In the surface variant it sits inside the frame. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
