Table
Displays rows of data in columns, with optional sortable and pinned headers.
Basic
| Name | Account type | |
|---|---|---|
Astra | astra.terra@example.com | Admin |
Nova | nova.star@example.com | Member |
Lyra | lyra.constellation@example.com | Guest |
Show codeHide code
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 mode="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>
);
}Spreadsheet
mode="spreadsheet" fully borders every cell into a grid, in contrast to the default mode="table" which keeps row dividers only.
| Name | Display name | Account type | |
|---|---|---|---|
Astra | astra | astra.terra@example.com | Admin |
Nova | nova | nova.star@example.com | Member |
Lyra | lyra | lyra.constellation@example.com | Guest |
Show codeHide code
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@makeplane/propel/components/table";
const COLUMNS = ["Name", "Display name", "Email", "Account type"];
const PEOPLE = [
{ name: "Astra", display: "astra", email: "astra.terra@example.com", role: "Admin" },
{ name: "Nova", display: "nova", email: "nova.star@example.com", role: "Member" },
{ name: "Lyra", display: "lyra", email: "lyra.constellation@example.com", role: "Guest" },
];
export default function SpreadsheetDemo() {
return (
<Table mode="spreadsheet">
<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.display}
</TableCell>
<TableCell pinned="none" padding="cell">
{person.email}
</TableCell>
<TableCell pinned="none" padding="cell">
{person.role}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}Sortable
A sortable header renders its label as a button with a sort chevron and reflects the order through aria-sort. Clicking cycles none → asc → desc.
| Account type | ||
|---|---|---|
Astra | astra.terra@example.com | Admin |
Nova | nova.star@example.com | Member |
Lyra | lyra.constellation@example.com | Guest |
Show codeHide code
import {
Table,
TableBody,
TableCell,
TableHead,
type TableHeadSort,
TableHeader,
TableRow,
} from "@makeplane/propel/components/table";
import * as React from "react";
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 SortableDemo() {
const [sort, setSort] = React.useState<TableHeadSort>("none");
const cycle = () => setSort((s) => (s === "none" ? "asc" : s === "asc" ? "desc" : "none"));
const rows =
sort === "none"
? PEOPLE
: [...PEOPLE].sort((a, b) => a.name.localeCompare(b.name) * (sort === "asc" ? 1 : -1));
return (
<Table mode="table">
<TableHeader>
<TableRow>
<TableHead pinned="none" label="Name" sortable sort={sort} onSort={cycle} />
<TableHead pinned="none" label="Email" />
<TableHead pinned="none" label="Account type" />
</TableRow>
</TableHeader>
<TableBody>
{rows.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>
);
}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.
| Name | Account type | Actions | |
|---|---|---|---|
A Astra | astra.terra@example.com | ||
N Nova | nova.star@example.com | ||
L Lyra | lyra.constellation@example.com |
Show codeHide code
import { Avatar } from "@makeplane/propel/components/avatar";
import { Icon } from "@makeplane/propel/components/icon";
import { MenuContent, MenuItem } from "@makeplane/propel/components/menu";
import {
Table,
TableActionCell,
TableBody,
TableCell,
TableEditableCell,
TableHead,
TableHeader,
TableRow,
} from "@makeplane/propel/components/table";
import { Pencil, Trash2 } from "lucide-react";
import * as React from "react";
const ROLES = ["Admin", "Member", "Guest"];
const INITIAL = [
{ 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 RichRowsDemo() {
const [people, setPeople] = React.useState(INITIAL);
const setRole = (email: string, role: string) =>
setPeople((rows) => rows.map((r) => (r.email === email ? { ...r, role } : r)));
return (
<Table mode="table">
<TableHeader>
<TableRow>
<TableHead pinned="none" label="Name" />
<TableHead pinned="none" label="Email" />
<TableHead pinned="none" label="Account type" />
<TableHead pinned="none" label="Actions" visuallyHidden />
</TableRow>
</TableHeader>
<TableBody>
{people.map((person) => (
<TableRow key={person.email}>
<TableCell
pinned="none"
padding="cell"
startIcon={
<Avatar magnitude="xs" alt={person.name} fallback={person.name.charAt(0)} />
}
>
{person.name}
</TableCell>
<TableCell pinned="none" padding="cell">
{person.email}
</TableCell>
<TableEditableCell value={person.role} aria-label={`Account type for ${person.name}`}>
<MenuContent>
{ROLES.map((role) => (
<MenuItem
key={role}
label={role}
selected={role === person.role}
onClick={() => setRole(person.email, role)}
/>
))}
</MenuContent>
</TableEditableCell>
<TableActionCell aria-label={`Options for ${person.name}`}>
<MenuContent>
<MenuItem icon={<Icon icon={Pencil} tint="secondary" />} label="Edit" />
<MenuItem tone="danger" icon={<Icon icon={Trash2} />} label="Delete" />
</MenuContent>
</TableActionCell>
</TableRow>
))}
</TableBody>
</Table>
);
}Installation
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from "@makeplane/propel/components/table";
Props
Table
| Prop | Type | Required | Description |
|---|---|---|---|
| render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}> | No | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| mode | "table" | "spreadsheet" | Yes | Layout (required). `table` draws row dividers only; `spreadsheet` draws a full grid. |
TableHead
| Prop | Type | Required | Description |
|---|---|---|---|
| render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}> | No | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| pinned | "none" | "start" | "end" | Yes | |
| label | string | Yes | Header label. |
| visuallyHidden | boolean | No | Visually hide the label while keeping it available to assistive tech. |
| sortable | boolean | No | Whether this header is interactive sortable (renders a sort trigger + reflects `aria-sort`). |
| sort | "desc" | "none" | "asc" | No | Current sort state for a sortable header. |
| onSort | (() => void) | No | Click handler for the sort control; only used when the header is sortable. |
TableCell
| Prop | Type | Required | Description |
|---|---|---|---|
| render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}> | No | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| pinned | "none" | "start" | "end" | Yes | |
| padding | "cell" | "trigger" | Yes | |
| startIcon | ReactNode | No | Leading element beside the cell text, e.g. `<Icon icon={...} />` or an `Avatar`. |
| endIcon | ReactNode | No | Trailing element beside the cell text, e.g. `<Icon icon={...} />` or an `Avatar`. |
| children | ReactNode | No | Cell content. |