Table
Composable table primitives (header, body, footer, rows, cells and caption) with hover rows, a selected state and built-in horizontal scroll.
| Endpoint | Region | p95 |
|---|---|---|
| /v1/deployments | us-east-1 | 48ms |
| /v1/webhooks | eu-central-1 | 71ms |
| /v1/keys/rotate | ap-south-1 | 63ms |
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
export default function TableDemo() {
return (
<Table variant="striped">
<TableHeader>
<TableRow>
<TableHead>Endpoint</TableHead>
<TableHead>Region</TableHead>
<TableHead className="text-right">p95</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-mono text-xs">/v1/deployments</TableCell>
<TableCell>us-east-1</TableCell>
<TableCell className="text-right">48ms</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-xs">/v1/webhooks</TableCell>
<TableCell>eu-central-1</TableCell>
<TableCell className="text-right">71ms</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-xs">/v1/keys/rotate</TableCell>
<TableCell>ap-south-1</TableCell>
<TableCell className="text-right">63ms</TableCell>
</TableRow>
</TableBody>
</Table>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/tableDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/table.tsx"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
type TableDensity = "compact" | "comfortable" | "relaxed"
type TableVariant = "line" | "striped" | "outline"
const TableContext = React.createContext<{
density: TableDensity
variant: TableVariant
}>({ density: "comfortable", variant: "line" })
interface TableProps extends React.ComponentProps<"table"> {
density?: TableDensity
variant?: TableVariant
}
function Table({
className,
density = "comfortable",
variant = "line",
...props
}: TableProps) {
return (
<TableContext.Provider value={{ density, variant }}>
<div
data-slot="table-container"
className={cn(
"relative w-full overflow-x-auto",
variant === "outline" && "rounded-lg border border-border"
)}
>
<table
data-slot="table"
data-density={density}
data-variant={variant}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
</TableContext.Provider>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b [&_tr]:border-border", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
const { variant } = React.useContext(TableContext)
return (
<tbody
data-slot="table-body"
className={cn(
"[&_tr:last-child]:border-0",
variant === "striped" && "[&_tr:nth-child(even)]:bg-surface-2/60",
className
)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn("border-t border-border bg-surface-2 font-medium", className)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
const { variant } = React.useContext(TableContext)
return (
<tr
data-slot="table-row"
className={cn(
"transition-colors duration-(--motion-fast) hover:bg-surface-2 data-[state=selected]:bg-accent",
variant !== "striped" && "border-b border-border",
className
)}
{...props}
/>
)
}
const densityHead: Record<TableDensity, string> = {
compact: "h-8 px-2",
comfortable: "h-10 px-3",
relaxed: "h-12 px-4",
}
const densityCell: Record<TableDensity, string> = {
compact: "p-2",
comfortable: "p-3",
relaxed: "p-4",
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
const { density, variant } = React.useContext(TableContext)
return (
<th
data-slot="table-head"
className={cn(
"text-left align-middle text-xs font-medium text-muted-foreground [&:has([role=checkbox])]:pe-0",
densityHead[density],
variant === "outline" && "border-r border-border last:border-r-0",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
const { density, variant } = React.useContext(TableContext)
return (
<td
data-slot="table-cell"
className={cn(
"align-middle [&:has([role=checkbox])]:pe-0",
densityCell[density],
variant === "outline" && "border-r border-border last:border-r-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", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
type TableProps,
}Manual installs skip the @ai2/tokens theme - add the token CSS from the theming guide or the tone colors will be missing.
Usage
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
<Table>
<TableHeader>
<TableRow>
<TableHead>Endpoint</TableHead>
<TableHead className="text-right">p95</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-mono text-xs">/v1/deployments</TableCell>
<TableCell className="text-right">48ms</TableCell>
</TableRow>
</TableBody>
</Table>The parts mirror native table elements one-to-one. The root Table wraps itself in a scroll container, so wide tables overflow horizontally instead of breaking the layout. Wrap it in your own data logic (e.g. TanStack Table) or use it directly.
Examples
Density
| Branch | Region | Status |
|---|---|---|
| main | us-east-1 | Deployed |
| feat/billing | eu-central-1 | Building |
| fix/webhooks | ap-south-1 | Queued |
| Branch | Region | Status |
|---|---|---|
| main | us-east-1 | Deployed |
| feat/billing | eu-central-1 | Building |
| fix/webhooks | ap-south-1 | Queued |
| Branch | Region | Status |
|---|---|---|
| main | us-east-1 | Deployed |
| feat/billing | eu-central-1 | Building |
| fix/webhooks | ap-south-1 | Queued |
density sets header and cell padding across the whole table: compact for dense dashboards, comfortable (the default) and relaxed for roomier reading. It flows to every cell through context.
Variants
| Branch | Region | Status |
|---|---|---|
| main | us-east-1 | Deployed |
| feat/billing | eu-central-1 | Building |
| fix/webhooks | ap-south-1 | Queued |
| Branch | Region | Status |
|---|---|---|
| main | us-east-1 | Deployed |
| feat/billing | eu-central-1 | Building |
| fix/webhooks | ap-south-1 | Queued |
| Branch | Region | Status |
|---|---|---|
| main | us-east-1 | Deployed |
| feat/billing | eu-central-1 | Building |
| fix/webhooks | ap-south-1 | Queued |
variant controls row separation: line (the default) divides rows with a border, striped zebra-stripes even rows, and outline draws a boxed grid with cell borders.
Footer and caption
| Invoice | Status | Amount |
|---|---|---|
| INV-0042 | Paid | $250.00 |
| INV-0043 | Pending | $150.00 |
| Total | $400.00 | |
Selected rows
| Branch | Status |
|---|---|
| main | Deployed |
| feat/billing | Building |
Set data-state="selected" on a TableRow to highlight it - selection libraries like TanStack Table set this attribute for you.
Props
Table adds the two axes below and passes them to every head and cell through context; all parts also accept every native prop of their underlying element.
| Prop | Type | Default | Description |
|---|---|---|---|
density | "compact" | "comfortable" | "relaxed" | "comfortable" | Row and cell spacing. compact tightens padding for dense dashboards; relaxed opens it up. Flows to every head and cell via context. |
variant | "line" | "striped" | "outline" | "line" | Row separation style. line divides rows with a bottom border; striped zebra-stripes even rows; outline draws a boxed grid with cell borders. |
The remaining seven parts add no props of their own: TableHeader (<thead>), TableBody (<tbody>), TableFooter (<tfoot>), TableRow (<tr>), TableHead (<th>), TableCell (<td>) and TableCaption (<caption>).
ai2 Table: semantic data tables for React, tuned for density
The ai2 Table is a shadcn-compatible table component for React, built on plain semantic HTML table elements and styled with Tailwind CSS v4. It gives you composable primitives for headers, bodies, footers, rows, cells and captions, tuned for the data density of dashboards, billing pages and admin screens: compact paddings, quiet muted headers and a hover highlight on every row.
Because it ships through the shadcn registry format, you install it with one CLI command, an MCP agent, or a copy-paste, and the source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Table?
It is an eight-part composition that mirrors native table elements one-to-one: Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell and TableCaption. The anatomy matches shadcn/ui exactly, so existing snippets, muscle memory and AI agents keep working without changes.
The root wraps the table in an overflow container, so wide tables scroll horizontally instead of breaking the page layout. A density axis (compact, comfortable, relaxed) and a variant axis (line, striped, outline) flow from the root to every cell through context. Styling comes from the ai2 token file, which means header text, borders, the footer surface and the row hover follow your theme in both light and dark mode from day one.
Why use it
- Semantic HTML underneath: Each part renders its native element (table, thead, tbody, tfoot, tr, th, td, caption), so screen readers, keyboard users and search engines get real table semantics for free.
- Density axis for data: A density prop (compact, comfortable, relaxed) sets header and cell padding across the whole table, so you tune large datasets to fit without custom overrides.
- Row states built in: Rows highlight on hover and switch to the accent surface when data-state="selected" is set, which is exactly the attribute selection libraries like TanStack Table emit.
- Horizontal scroll by default: The root wraps itself in an overflow-x-auto container, so a wide table scrolls in place instead of blowing up your grid.
- Agent-readable metadata: The registry item describes its parts and intended use in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component, its dependencies and the @ai2/tokens theme to your project.
- Eight composable parts: Header, body, footer, row, head cell, data cell and caption compose freely, so you only render the parts a given table needs.
- Footer and caption support: TableFooter gets a surface-2 background and medium weight for totals; TableCaption renders below the table in muted text.
- Checkbox-ready cells: TableHead and TableCell drop their right padding automatically when they contain a checkbox, so selection columns align tightly.
- Motion from tokens: The row hover transition reads --motion-fast from the shared token file instead of a hardcoded duration, matching every other ai2 component.
- Data attributes for styling: Every part exposes data-slot (table, table-header, table-row and so on), so you can restyle any layer from CSS without forking the component.
Production tips
- Add a caption for context: TableCaption is the accessible name of the table. A short caption like "Invoice totals for June 2026" helps screen reader users and costs one line.
- Right-align numbers: Add text-right to numeric TableHead and TableCell pairs so amounts and latencies line up on the decimal side and scan faster.
- Let libraries drive selection: Do not hand-roll selected state. Wire TanStack Table or your own logic to set data-state="selected" on TableRow and the accent highlight follows.
- Mind very wide tables on mobile: The built-in horizontal scroll saves the layout, but consider hiding low-priority columns at small breakpoints instead of making users pan.
- Use the footer for totals only: TableFooter is styled as a summary surface. Keep repeated data rows in TableBody so the visual hierarchy stays honest.
Works with the rest of ai2
Tables get better with the rest of the registry. Put an ai2 Badge in a status column, an ai2 Checkbox in the selection column (the cells already handle its padding), and an ai2 Dropdown Menu behind a row actions button.
For page-level composition, follow the table with ai2 Pagination for long datasets, and show ai2 Skeleton rows while the data loads. Everything shares one token source, so combinations stay visually consistent in both modes.