Expandable Rows
Free expandable table rows for React and Next.js, animated with pure CSS.
| Order | Customer | Total | Status | Details |
|---|---|---|---|---|
| TE-1042 | Aurora Studio | $1,240.00 | Shipped | |
| ||||
| TE-1041 | Northwind Labs | $386.50 | Processing | |
| ||||
| TE-1040 | Halbon Systems | $2,980.00 | Delivered | |
| ||||
| TE-1039 | Fieldline Co | $149.00 | Refunded | |
| ||||
| TE-1038 | Meridian Press | $720.00 | Shipped | |
| ||||
Expandable Rows is a free React table component for Next.js that adds expandable detail rows to a real HTML table, styled with Tailwind and animated with zero JavaScript animation code. Each data row carries a trailing toggle button; activating it unfolds a full width detail panel beneath the row by interpolating a CSS grid wrapper's grid-template-rows from 0fr to 1fr while the chevron rotates ninety degrees in sync.
Semantics stay pure table throughout: detail panels are real tr and td elements with colSpan, never divs pretending to be rows, so screen readers and crawlers see an ordinary table. Any number of rows can be open at once, each one independent, and a required stable row id keeps expansion attached to the same record through inserts, removals, and reordering. Closing panels become inert immediately, then transition visibility to hidden after the fold completes; with reduced motion enabled every toggle is instant.
It ships in the same card shell as the other tables in this category: a bordered panel around an inset scroll region, with the caption set as the table's leading element above its supporting line. A cell whose value is a label and tone pair renders as a status pill, with a tone-coloured dot and tint behind a foreground label, so a state is never signalled by colour alone and pill text keeps the panel's own contrast in both themes.
Install
Add the @te registry to your components.json once:
{
"registries": {
"@te": "https://templateempire.io/r/{name}.json"
}
}Then install the component:
npx shadcn@latest add @te/table-expand-rowsUsage
import { TableExpandRows } from "@/components/table-expand-rows";
const columns = [
{ key: "order", label: "Order" },
{ key: "customer", label: "Customer" },
{ key: "total", label: "Total", align: "right" as const },
{ key: "status", label: "Status" },
];
const rows = [
{
id: "TE-1042",
cells: {
order: "TE-1042",
customer: "Aurora Studio",
total: "$1,240.00",
status: { label: "Shipped", tone: "accent" as const },
},
detail: <p>3 x Studio Display Arm, shipped via DHL Express.</p>,
},
{
id: "TE-1041",
cells: {
order: "TE-1041",
customer: "Northwind Labs",
total: "$386.50",
status: { label: "Processing", tone: "warning" as const },
},
detail: <p>2 x Mechanical Keyboard, picking in Rotterdam.</p>,
},
];
export default function Page() {
return (
<TableExpandRows
caption="Recent orders"
description="Last five orders across the EU fulfilment network, newest first."
columns={columns}
rows={rows}
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| columns | { key: string; label: string; align?: "left" | "right" }[] | required | Column definitions; each key indexes into every row's cells record and each label renders as a column header. A right-aligned column also renders tabular figures, so amounts line up across rows. |
| rows | { id: string; cells: Record<string, ReactNode | { label: string; tone?: Tone }>; detail: ReactNode }[] | required | Row data with a unique stable id plus the detail panel content. The id keeps expansion attached to its record when rows reorder. A cell holding a label and tone pair renders as a status pill; tone is neutral, accent, positive, warning or critical. |
| caption | string | undefined | Rendered as the table's visible caption, set as the largest text in the shell so the table has one unambiguous leading element. |
| description | string | undefined | One supporting line beneath the caption, inside the same caption element. |
| radius | number | 20 | Shell corner radius in CSS pixels, clamped to 0 through 40, matching the other tables in this category. |
| className | string | undefined | Additional classes merged onto the shell. |
Customisation
Expandable Rows reads the shared motion tokens for the unfold and a small set of its own knobs for the shell and the status tones, each one resolving to a semantic token so the table follows your theme with no per-component work. The status ramp is derived from your alert token, so it stays coherent in light and dark:
<TableExpandRows columns={columns} rows={rows} radius={12} />
:root {
/* shell and hairlines */
--expand-table-panel: var(--card);
--expand-table-divider: var(--border);
/* status tones: dots and tints, never pill text */
--expand-table-positive: oklch(from var(--destructive) l c calc(h + 120));
--expand-table-warning: oklch(from var(--destructive) l c calc(h + 55));
--expand-table-critical: var(--destructive);
--expand-table-accent-text: var(--primary-accent);
/* slower, softer unfold everywhere the tokens are consumed */
--dur-3: 800ms;
--ease-out-quart: cubic-bezier(0.22, 1, 0.36, 1);
}Frequently asked questions
- Is Expandable Rows free for commercial use?
- Yes. Expandable Rows is MIT licensed, so you can use it in personal and commercial projects without attribution.
- Does Expandable Rows stay accessible while rows are collapsed?
- Yes. Every toggle is a real button with aria-expanded and a name that references its row, and detail panels are real table rows with a single spanned cell, so table semantics are never faked. A closing panel becomes inert immediately, removing its content from focus and accessibility navigation while its visibility waits for the visual fold to finish. The animation interpolates grid-template-rows in pure CSS, and with reduced motion enabled both directions toggle instantly.
- Can several rows be open at the same time?
- Yes. Each row's required id owns its expanded state, so any number of detail panels can be open at once, reordering the rows keeps the right record open, and toggling one row never closes another. IDs must be unique and stable for the lifetime of each record. There is no accordion mode or controlled expansion API.