Table
A complete primitive set for tabular data — header, body, footer, sortable columns, sticky header, row states, plus cell helpers for avatars, status, currency amounts, and actions. Designed in the Wise / PrimeReact tradition.
Playground
Installation
pnpm add @tessinaui/uiUsage
import {
TableRoot,
TableHeader,
TableBody,
TableFooter,
TableRow,
TableHead,
TableCell,
TableCaption,
TableEmpty,
TableCellAvatar,
TableCellAmount,
TableCellStatus,
TableCellActions,
} from "@tessinaui/ui";<TableRoot variant="lined" hoverable>
<TableHeader>
<TableRow hoverable={false}>
<TableHead sortable sortDirection="asc" onSort={handleSort("name")}>User</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead numeric>Amount</TableHead>
<TableHead align="end" />
</TableRow>
</TableHeader>
<TableBody>
{users.map((u) => (
<TableRow key={u.id}>
<TableCell>
<TableCellAvatar
avatar={<Avatar size="sm" initials={initialsOf(u.name)} />}
name={u.name}
secondary={u.email}
/>
</TableCell>
<TableCell>{u.role}</TableCell>
<TableCell>
<TableCellStatus intent="success" label="Active" />
</TableCell>
<TableCell numeric>
<TableCellAmount currency="$" amount={u.amount.toFixed(2)} />
</TableCell>
<TableCell align="end">
<TableCellActions>
<IconButton variant="ghost" size="xs" icon={<MoreHorizontal />} aria-label="Actions" />
</TableCellActions>
</TableCell>
</TableRow>
))}
</TableBody>
</TableRoot>Examples
Default
A team table composing avatar, status, currency, and action cell helpers.
Variants
Four visual treatments — lined, striped, bordered, and borderless.
Sizes
Five density steps from xs to xl scale cell padding and text size.
Row states
Rows can be selected, disabled, or destructive.
Sortable columns
Sortable headers cycle through none → asc → desc and re-sort the rows.
Empty & loading
TableEmpty fills the body when there's no data; TableSkeleton covers the loading state.
When to use
Table is the workhorse for any structured tabular data — users, transactions, orders, audit logs, settings rows, etc. It's a primitive set: bring your own data fetching, pagination, and selection state, and Tessina handles the visual layer plus sorting indicators, sticky header, and the cell patterns that show up everywhere.
For a turnkey component with built-in filtering / pagination / selection, layer TanStack Table on top of these primitives — the styling will carry through unchanged.
API Reference
TableRoot props
| Prop | Type | Default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Cell padding + text size |
variant | "lined" | "striped" | "bordered" | "borderless" | "lined" | Visual treatment — lined is default, striped adds zebra rows, bordered adds vertical separators, borderless removes the container chrome |
rounded | "none" | "sm" | "md" | "lg" | "full" | "md" | Container corner radius |
intent | "none" | "primary" | "error" | "warning" | "success" | "info" | "none" | Subtle border accent on the container |
dir | "ltr" | "rtl" | "ltr" | Text direction — flips alignment and sticky-column side |
hoverable | boolean | true | Highlight rows on hover |
stickyHeader | boolean | false | Stick the header to the top of the scroll container |
withContainer | boolean | true | Wrap the <table> in a TableContainer (you can disable to use your own) |
TableContainer props
A standalone scroll container — useful if you need to wrap a table you didn't build with TableRoot.
| Prop | Type | Default | Description |
|---|---|---|---|
size, variant, rounded, intent | same as Root | — | Visual chrome |
scrollable | boolean | true | Add horizontal overflow scrolling |
TableHeader / TableBody / TableFooter
Semantic <thead> / <tbody> / <tfoot> wrappers. Header gets the muted background; footer gets the same styling for totals rows.
TableRow props
| Prop | Type | Default | Description |
|---|---|---|---|
selected | boolean | false | Highlights the row in the primary tint |
disabled | boolean | false | Greys out the row and disables pointer events |
destructive | boolean | false | Tints the row in error colour (e.g. flagged for deletion) |
hoverable | boolean | inherited | Override the parent's hoverable setting for this row |
TableHead props
| Prop | Type | Default | Description |
|---|---|---|---|
align | "start" | "center" | "end" | "start" | Text alignment |
numeric | boolean | false | Right-align with tabular numerals |
sortable | boolean | false | Show a sort indicator and accept clicks to cycle |
sortDirection | "asc" | "desc" | "none" | "none" | Current sort direction (controlled) |
onSort | (next: SortDirection) => void | — | Called when the user clicks the indicator. Receives the next direction in the cycle (none → asc → desc → none) |
sticky | boolean | false | Stick this column to the start (left in LTR, right in RTL) |
width | number | string | — | Column width hint |
Sets aria-sort automatically when sortable is true.
TableCell props
Same align, numeric, sticky, width props as TableHead, plus truncate for ellipsis overflow.
TableEmpty props
| Prop | Type | Default | Description |
|---|---|---|---|
colSpan | number | required | Number of columns in the table — required so the cell spans correctly |
icon | ReactNode | — | Optional icon shown above the message |
title | ReactNode | — | Bold title above the body text |
Renders a single row with one cell that spans the table.
Cell helpers
These compose inside TableCell and cover the patterns that show up in nearly every product table.
TableCellAvatar
<TableCell>
<TableCellAvatar
avatar={<Avatar src={user.photo} initials={initialsOf(user.name)} />}
name={user.name}
secondary={user.email}
/>
</TableCell>| Prop | Description |
|---|---|
avatar | Slot for any avatar component |
name | Primary text (bold) |
secondary | Optional second line (muted) |
TableCellAmount
Right-aligns a currency amount with optional comparison delta (tinted by intent).
<TableCell numeric>
<TableCellAmount
currency="$"
amount="1,240.50"
delta="+12.4%"
deltaIntent="success"
/>
</TableCell>| Prop | Description |
|---|---|
amount | Main number (tabular numerals) |
currency | Optional symbol or code shown before |
delta | Optional comparison line below |
deltaIntent | "none" | "success" | "error" |
TableCellStatus
Dot + label, in 6 intents (neutral, primary, error, warning, success, info).
<TableCell>
<TableCellStatus intent="success" label="Active" />
</TableCell>TableCellActions
A trailing-edge container for icon-button actions.
<TableCell align="end">
<TableCellActions>
<IconButton variant="ghost" size="xs" icon={<Pencil />} aria-label="Edit" />
<IconButton variant="ghost" size="xs" icon={<Trash />} aria-label="Delete" />
</TableCellActions>
</TableCell>TableCellText
Two-line cell with primary + muted secondary.
<TableCell>
<TableCellText primary="Wire transfer" secondary="ACH · 2 days" />
</TableCell>Notes
- The
<table>element renders inside aTableContainerby default, which provides the rounded card chrome, intent border, and horizontal scroll for narrow viewports. PasswithContainer={false}if you want to nest the table inside your own container (e.g. a Card). aria-sort="ascending" | "descending" | "none"is set automatically on sortable headers.- For huge datasets, virtualize the body with TanStack Virtual or a similar library — the primitives don't impose any virtualization but compose naturally with one.
- For RTL: alignment, sortable indicators, and sticky-column side all flip via the
dirprop. Numbers and currency stay LTR-formatted (which is the typographic norm even in Arabic-script copy).