Table
Displays rows of data in columns, with optional sortable headers and pinned columns.
Show codeHide code
Installation
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
TableActionCell,
TableEditableCell,
} from "@makeplane/propel/components/table";Usage
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@makeplane/propel/components/table";
const COLUMNS = ["Name", "Email", "Account type"];
const PEOPLE = [
{ name: "Astra", email: "astra.terra@example.com", role: "Admin" },
{ name: "Nova", email: "nova.star@example.com", role: "Member" },
{ name: "Lyra", email: "lyra.constellation@example.com", role: "Guest" },
];
export default function BasicDemo() {
return (
<Table variant="table">
<TableHeader>
<TableRow>
{COLUMNS.map((c) => (
<TableHead key={c} pinned="none" label={c} />
))}
</TableRow>
</TableHeader>
<TableBody>
{PEOPLE.map((person) => (
<TableRow key={person.email}>
<TableCell pinned="none" padding="cell">
{person.name}
</TableCell>
<TableCell pinned="none" padding="cell">
{person.email}
</TableCell>
<TableCell pinned="none" padding="cell">
{person.role}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}Table requires variant (table | spreadsheet | plain). Plain TableHead / TableCell require pinned
(none | start | end). Plain cells also require padding (cell for normal content, trigger
when a full-cell control fills the cell). TableActionCell and TableEditableCell set
pinned="none" and padding="trigger" for you.
Name the table for assistive tech with a preceding heading, or pass aria-label / aria-labelledby
on Table when the page has more than one data table. Header cells default to scope="col"; for a
leading column that names each row, pass scope="row" on that cell.
Examples
Spreadsheet
variant="spreadsheet" fully borders every cell into a grid, in contrast to variant="table" which
keeps row dividers only.
Show codeHide code
Plain
variant="plain" keeps the table metrics and removes the frame and the fills, for a table that
sits directly on the page. Every row keeps its divider, the last one included. The header and
pinned cells are sticky, so they paint an opaque fill, surface-1 by default. On any other surface,
set the --table-surface CSS custom property on an ancestor of the table to that surface’s fill.
This example sits on a layer-2 pane, so its wrapper sets --table-surface: var(--bg-layer-2).
Show codeHide code
Expandable rows
Use TableExpandCell as the first cell of every row. A row with nested rows passes expanded,
onExpandedChange and an aria-label for the chevron button (keep the name the same in both states;
aria-expanded announces the state); a row without any passes none of
them and gets a spacer of the same size, so the titles stay aligned. Nested rows are ordinary
TableRows rendered after their parent while it is expanded, with depth={1} on their first cell.
inset="expand" on the column’s TableHead lines the header label up with the row content. It
works in every variant.
TableNestedRows renders the nested rows for you. It shows the first maxVisible (5 by default)
and, for a longer run, a “show more” row that reveals the rest and moves focus to the first row it
revealed. Collapsing the parent resets it. It is optional: to page nested rows from a server or
reveal them a page at a time, compose TableRow and TableShowMoreCell yourself.
The table holds no expand state. You own which rows are open.
Show codeHide code
Sortable
A header with sortable and onSort renders its label as a button with a sort chevron. Clicking
cycles none → asc → desc; aria-sort is set for asc/desc and omitted while unsorted.
Show codeHide code
Rich rows
Cells can carry a leading Avatar, an inline TableEditableCell that opens a menu to change its
value, and a trailing icon-only TableActionCell for row actions. While an editable cell’s menu is
open, pass selected for the stronger open tint — it’s visual only; the menu trigger already
exposes open state via aria-expanded.
Show codeHide code
Large tables
Table paints the rows you pass — it is not a data grid. Prefer pagination or
app-side virtualization for many rows. Each TableEditableCell / TableActionCell mounts its own
Menu; at high row counts, share one menu with
createMenuHandle instead of a menu per cell.
Cell slots and disabled
TableCell accepts endIcon (and startIcon) for trailing chrome. TableEditableCell and
TableActionCell accept disabled so the full-cell trigger cannot open its menu.
Show codeHide code
With pagination
Table and Pagination are separate — slice the rows for the current page and drive page /
pageSize from the pager below.
Show codeHide code
Pinned columns
In a height- and width-constrained frame the header stays pinned to the top on vertical scroll, and a column whose head and cells set pinned="start" or pinned="end" stays put on horizontal scroll.
Show codeHide code
API Reference
Table
The ready-made table: the styled <table> wrapped in a scroll frame (rounded and
hairline-bordered, or bare for variant="plain"), sharing its layout variant with the
rows/cells/heads via context. The Base UI ScrollArea behavior grafts onto the styled
TableScrollArea frame + viewport (and the overlay scrollbars) via render, behavior part
outer.
TableHeader
Header section (<thead>). Holds a single TableRow of TableHead cells.
TableBody
Body section (<tbody>). Holds the data TableRows.
TableRow
A ready-made table row (<tr>), taking its resting fill from the surrounding Table's
variant. selected paints the selected tint, interactive marks the row as a click target,
and section makes it a non-data row that labels the rows beneath it.
selected is a visual state only. Pair it with a TableSelectCell, whose checkbox carries the
real state.
TableHead
A ready-made header cell: a plain title, arbitrary children, or (when sortable) either of those
inside a sort-cycling button with a chevron.
TableCell
A ready-made data cell: optional leading/trailing slots around a truncating content region.
TableExpandCell
The first cell (<td>) of a row in a table with nested rows. With expanded it leads with a
chevron button that reveals or hides the row's nested rows; without it, with an empty spacer of
the same size. depth indents the cell for a nested row.
The cell owns no state and renders no nested rows. The consumer holds which rows are open and
renders the nested TableRows after the parent while it is expanded, the same way it owns the
sort order. The table stays a plain <table>: the control is a button with aria-expanded, and
focus stays on it after it is activated.
Activating the control does not reach the row: its click, and the Enter or Space keydown that
activates it, stop at the control, so an interactive row's own click or keyboard handler does
not also fire.
TableNestedRows
The nested rows under an expanded parent, with the "show more" behaviour built in. It renders the
first maxVisible rows and, while any are hidden, a TableShowMoreCell row that reveals the
rest. Render it after the parent TableRow while the parent is expanded:
``tsx
{expanded ? <TableNestedRows items={page.children} … /> : null}
It renders rows only, with no wrapper element, so it sits directly in a TableBody. Revealing
the rest removes the button, so focus moves to the first row it revealed instead of being lost.
Collapsing the parent unmounts the group, so it opens on its first maxVisible rows again.
It is optional. For anything it does not cover — paging nested rows from a server, revealing a
page at a time — compose TableRow, TableExpandCell and TableShowMoreCell` directly.
TableShowMoreCell
A cell (<td>) holding a text button that reveals the rest of a long run of nested rows. Put it
alone in a TableRow after the nested rows shown so far, and pass colSpan so it spans the
table. It is indented to line up with the nested rows' content.
Like the expand control, activating the button does not reach the row: its click, and the Enter
or Space keydown that activates it, stop at the button, so an interactive row's own handler
does not also fire.
The cell does not count rows: the consumer decides how many nested rows to render first and what
the label says.
TableActionCell
An icon-only action cell (<td>) that opens a row-actions menu.
TableEditableCell
An editable data cell (<td>) with a full-cell menu trigger.