Skip to content
Template Empire
Browse components

Expandable Rows

Free expandable table rows for React and Next.js, animated with pure CSS.

Recent ordersLast five orders across the EU fulfilment network, newest first.
OrderCustomerTotalStatusDetails
TE-1042Aurora Studio$1,240.00Shipped
Items
3 x Studio Display Arm, 1 x Desk Mat
Carrier
DHL Express, 1 to 2 business days
TE-1041Northwind Labs$386.50Processing
Items
2 x Mechanical Keyboard, 65 percent
Warehouse
Rotterdam, picking since 08:15
Note
Gift wrap requested on both units
TE-1040Halbon Systems$2,980.00Delivered
Items
10 x Conference Speakerphone
Delivered
Signed by R. Okafor, dock 4
Invoice
Paid via ACH on net 30 terms
TE-1039Fieldline Co$149.00Refunded
Items
1 x Travel Router
Reason
Arrived with damaged packaging
Refund
Full amount returned to card
TE-1038Meridian Press$720.00Shipped
Items
4 x Archival Paper, A2 bundle
Carrier
UPS Ground, 3 to 5 business days
Note
Deliver to loading bay before noon

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-rows

Usage

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

PropTypeDefaultDescription
columns{ key: string; label: string; align?: "left" | "right" }[]requiredColumn 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 }[]requiredRow 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.
captionstringundefinedRendered as the table's visible caption, set as the largest text in the shell so the table has one unambiguous leading element.
descriptionstringundefinedOne supporting line beneath the caption, inside the same caption element.
radiusnumber20Shell corner radius in CSS pixels, clamped to 0 through 40, matching the other tables in this category.
classNamestringundefinedAdditional 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.

Related components

Want the full page this belongs to?Template Empire builds complete Next.js templates with this motion system throughout. Browse the paid templates that pair with these free components.Explore Template Empire templates