| Deliverables | |||
|---|---|---|---|
Emma Taylor | Oct 14 | In progress | |
| |||
Liam Johnson | Oct 12 | Waiting on you | |
Social pack | Sophia Brown | Oct 20 | Not started |
Daniel Wu | Oct 2 | Done | |
npx shadcn@latest add https://sdk-components.thesqd.com/r/expandable-table.jsonimport { ExpandableTable } from "@/components/blocks/expandable-table";import { Badge } from "@/components/ui/badge";
import { ExpandableTable } from "@/components/blocks/expandable-table";
const owners: Record<string, string> = { "1": "Emma Taylor", "4": "Daniel Wu" };
const dues: Record<string, string> = { "1": "2026-10-14", "4": "2026-10-02" };
export function Demo() {
return (
<ExpandableTable
nameLabel="Deliverables"
columns={[
{ key: "owner", label: "Owner", width: 140, align: "left", sortValue: (r) => owners[r.id] },
{ key: "due", label: "Due", width: 120, sortValue: (r) => dues[r.id] },
{ key: "status", label: "Status", width: 150, hideable: false },
]}
rows={[
{
id: "1",
name: "Sermon series graphic",
expandable: true,
summary: "1/3 steps",
cells: { owner: "Emma Taylor", due: "Oct 14", status: <Badge color="info">In progress</Badge> },
},
{
id: "4",
name: "Bulletin cover",
muted: true,
cells: { owner: "Daniel Wu", due: "Oct 2", status: <Badge color="success">Done</Badge> },
},
]}
defaultExpandedIds={["1"]}
renderExpanded={(row) => <StepList deliverableId={row.id} />}
/>
);
}ExpandableTable (Simple table's card, rows that open onto anything)
├── card-surface wrapper
│ ├── header (only when title / description / action / columnPicker)
│ │ ├── title + description
│ │ └── "Columns" menu + action
│ └── horizontal scroller (scroll-fade-x)
│ └── <table>
│ ├── <thead> (name column · one sortable <th> per shown column)
│ └── <tbody> (per row: <tr> with caret + name + summary + cells,
│ then, while open, one full-width <tr> with renderExpanded)
│
├── columns={[{ key, label, width?, align?, sortValue?, hideable?, className? }]}
├── rows={[{ id, name, cells?, summary?, expandable?, muted? }]}
└── props: nameLabel · renderExpanded · defaultExpandedIds · reveal
sort · defaultSort · onSortChange
hiddenColumns · defaultHiddenColumns · onHiddenColumnsChange · columnPicker
title · description · action · emptyTitle · emptyDescription| Deliverables | ||
|---|---|---|
Oct 2 | Done | |
Oct 12 | Waiting on you | |
Oct 14 | In progress | |
Social pack | Oct 20 | Not started |
| Deliverables | |||
|---|---|---|---|
No deliverables yet They show up here once the project is planned. | |||
ExpandableTable
| Prop | Type | Default | Description |
|---|---|---|---|
columns | Column[] | — | The columns after the name — see Column below. |
rows | Row[] | — | See Row below. |
nameLabel | string | "Name" | Header of the name column. |
renderExpanded | (row) => ReactNode | — | What an open row shows, in one full-width cell. Called only while open. |
defaultExpandedIds | string[] | — | Rows open on first render. |
reveal | { id: string; at: number } | null | — | Opens that row and scrolls it into view. A new `at` lands again on the same row. |
sort | { key, direction } | null | — | Controlled sort. Header clicks cycle ascending → descending → off. |
defaultSort | { key, direction } | null | — | Uncontrolled starting sort. |
onSortChange | (sort) => void | — | Fires on every header click — persist it to remember the sort. |
hiddenColumns | string[] | — | Controlled hidden column keys. |
defaultHiddenColumns | string[] | — | Uncontrolled starting hidden keys. |
onHiddenColumnsChange | (keys) => void | — | Fires when the picker hides or shows a column. |
columnPicker | boolean | false | A Columns menu in the header for every `hideable` column. |
title | ReactNode | — | Card header title. |
description | ReactNode | — | Muted line under the title. |
action | ReactNode | — | Right side of the header row. |
emptyTitle | string | "No data found" | Heading when there are no rows. |
emptyDescription | string | — | Body copy when there are no rows. |
className | string | — | Classes on the card. |
Column
| Prop | Type | Default | Description |
|---|---|---|---|
key | string | — | Matches a key in each row's `cells`. |
label | string | — | Header text, also the picker label. |
width | number | string | 120 | Fixed width. The name column takes the rest. |
align | "left" | "center" | "right" | "right" | Cell and header alignment. |
sortValue | (row) => string | number | null | — | Makes the column sortable. Empty values sort last both ways. |
hideable | boolean | true | False keeps the column out of the picker. |
className | string | — | Classes on each body cell. |
Row
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Stable key; also what `reveal` and `defaultExpandedIds` name. |
name | ReactNode | — | The first column. |
cells | Record<string, ReactNode> | — | Column key → cell. A missing key draws nothing. |
summary | ReactNode | — | Small muted text after the name of a row that opens. |
expandable | boolean | — | Draws the caret; needs `renderExpanded`. |
muted | boolean | — | Finished row: the name recedes, cells keep their colour. |