All components
Expandable Rows Table
tablesOrigin UI component.
responsive · 600px
Install
Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:
$
npx shadcn@latest add https://your-domain/r/comp-482.jsonUsage
import Cmp from "@/registry/origin-ui/comp-482";
export default function Demo() {
return <Cmp />;
}Component source
"use client";
import {
type ColumnDef,
flexRender,
getCoreRowModel,
getExpandedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { ChevronDownIcon, ChevronUpIcon, InfoIcon } from "lucide-react";
import { Fragment, useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
type Item = {
id: string;
name: string;
email: string;
location: string;
flag: string;
status: "Active" | "Inactive" | "Pending";
balance: number;
note?: string;
};
const columns: ColumnDef<Item>[] = [
{
cell: ({ row }) => {
return row.getCanExpand() ? (
<Button
{...{
"aria-expanded": row.getIsExpanded(),
"aria-label": row.getIsExpanded()
? `Collapse details for ${row.original.name}`
: `Expand details for ${row.original.name}`,
className: "size-7 shadow-none text-muted-foreground",
onClick: row.getToggleExpandedHandler(),
size: "icon",
variant: "ghost",
}}
>
{row.getIsExpanded() ? (
<ChevronUpIcon
aria-hidden="true"
className="opacity-60"
size={16}
/>
) : (
<ChevronDownIcon
aria-hidden="true"
className="opacity-60"
size={16}
/>
)}
</Button>
) : undefined;
},
header: () => null,
id: "expander",
},
{
cell: ({ row }) => (
<Checkbox
aria-label="Select row"
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
/>
),
header: ({ table }) => (
<Checkbox
aria-label="Select all"
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
/>
),
id: "select",
},
{
accessorKey: "name",
cell: ({ row }) => (
<div className="font-medium">{row.getValue("name")}</div>
),
header: "Name",
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "location",
cell: ({ row }) => (
<div>
<span className="text-lg leading-none">{row.original.flag}</span>{" "}
{row.getValue("location")}
</div>
),
header: "Location",
},
{
accessorKey: "status",
cell: ({ row }) => (
<Badge
className={cn(
row.getValue("status") === "Inactive" &&
"bg-muted-foreground/60 text-primary-foreground",
)}
>
{row.getValue("status")}
</Badge>
),
header: "Status",
},
{
accessorKey: "balance",
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue("balance"));
const formatted = new Intl.NumberFormat("en-US", {
currency: "USD",
style: "currency",
}).format(amount);
return <div className="text-right">{formatted}</div>;
},
header: () => <div className="text-right">Balance</div>,
},
];
export default function Component() {
const [data, setData] = useState<Item[]>([]);
useEffect(() => {
async function fetchPosts() {
const res = await fetch(
"https://raw.githubusercontent.com/origin-space/origin-images/refs/heads/main/users-01_fertyx.json",
);
const data = await res.json();
setData(data.slice(0, 5)); // Limit to 5 items
}
fetchPosts();
}, []);
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
getRowCanExpand: (row) => Boolean(row.original.note),
});
return (
<div>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow className="hover:bg-transparent" key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<Fragment key={row.id}>
<TableRow
data-state={row.getIsSelected() && "selected"}
key={row.id}
>
{row.getVisibleCells().map((cell) => (
<TableCell
className="whitespace-nowrap [&:has([aria-expanded])]:w-px [&:has([aria-expanded])]:py-0 [&:has([aria-expanded])]:pr-0"
key={cell.id}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
{row.getIsExpanded() && (
<TableRow>
<TableCell colSpan={row.getVisibleCells().length}>
<div className="flex items-start py-2 text-primary/80">
<span
aria-hidden="true"
className="me-3 mt-0.5 flex w-7 shrink-0 justify-center"
>
<InfoIcon className="opacity-60" size={16} />
</span>
<p className="text-sm">{row.original.note}</p>
</div>
</TableCell>
</TableRow>
)}
</Fragment>
))
) : (
<TableRow>
<TableCell className="h-24 text-center" colSpan={columns.length}>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<p className="mt-4 text-center text-muted-foreground text-sm">
Expanding sub-row made with{" "}
<a
className="underline hover:text-foreground"
href="https://tanstack.com/table"
rel="noopener noreferrer"
target="_blank"
>
TanStack Table
</a>
</p>
</div>
);
}Dependencies
@tanstack/react-table
Source: Origin UI