Row Expansion Table
Add expandable rows to show additional details inline.
Preview with Controlled State
View Full State Object
{}[]
[]
{
"pageIndex": 0,
"pageSize": 10
}{}Introduction
Section titled “Introduction”The Row Expansion Table allows users to expand rows to view more details inline. This is useful for showing additional information without navigating away or opening a modal.
Installation
Section titled “Installation”Install the DataTable core and add-ons for this example:
This example also uses card and separator from Shadcn UI:
First time using
@niko-table? See the Installation Guide to set up the registry.
For other add-ons or manual copy-paste, see the Installation Guide.
Prerequisites
Section titled “Prerequisites”We are going to build a table to show orders with expandable items. Here’s what our data looks like:
type OrderItem = { id: string productName: string category: string price: number quantity: number}
type Order = { id: string orderNumber: string customer: string email: string total: number status: "pending" | "processing" | "shipped" | "delivered" | "cancelled" date: string items: OrderItem[]}Basic Table with Row Expansion
Section titled “Basic Table with Row Expansion”Let’s start by building a table with expandable rows.
Column Definitions
Section titled “Column Definitions”First, we’ll define our columns with an expand column.
"use client"
import { DataTableColumnHeader } from "@/components/niko-table/components/data-table-column-header"import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"import { DataTableColumnSortMenu } from "@/components/niko-table/components/data-table-column-sort"import type { DataTableColumnDef } from "@/components/niko-table/types"import { Button } from "@/components/ui/button"import { ChevronRight, ChevronDown } from "lucide-react"
export type Order = { id: string orderNumber: string customer: string email: string total: number status: string date: string items: OrderItem[]}
// Expanded content componentfunction OrderDetails({ order }: { order: Order }) { return ( <div className="bg-muted/30 p-6"> <h3 className="font-semibold">Order Items</h3> <div className="space-y-2"> {order.items.map(item => ( <div key={item.id} className="flex justify-between"> <span>{item.productName}</span> <span>${item.price.toFixed(2)}</span> </div> ))} </div> </div> )}
export const columns: DataTableColumnDef<Order>[] = [ { id: "expand", header: () => null, cell: ({ row }) => { if (!row.getCanExpand()) return null return ( <Button variant="ghost" size="sm" onClick={row.getToggleExpandedHandler()} className="h-6 w-6 p-0" > {row.getIsExpanded() ? ( <ChevronDown className="h-3.5 w-3.5" /> ) : ( <ChevronRight className="h-3.5 w-3.5" /> )} </Button> ) }, size: 50, enableSorting: false, enableHiding: false, meta: { expandedContent: (order: Order) => <OrderDetails order={order} />, }, }, { accessorKey: "orderNumber", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Order #" /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), }, { accessorKey: "customer", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Customer" /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), }, { accessorKey: "total", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Total" /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), cell: ({ row }) => { const total = row.getValue("total") as number return <div className="font-mono">${total.toFixed(2)}</div> }, }, { accessorKey: "status", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Status" /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), },]<DataTable /> component
Section titled “<DataTable /> component”Next, we’ll create the table with row expansion enabled.
"use client"
import { DataTableRoot } from "@/components/niko-table/core/data-table-root"import { DataTable } from "@/components/niko-table/core/data-table"import { DataTableHeader, DataTableBody, DataTableEmptyBody,} from "@/components/niko-table/core/data-table-structure"import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"import { DataTableViewMenu } from "@/components/niko-table/components/data-table-view-menu"import { DataTableColumnHeader } from "@/components/niko-table/components/data-table-column-header"import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"import { DataTableColumnSortMenu } from "@/components/niko-table/components/data-table-column-sort"import type { DataTableColumnDef } from "@/components/niko-table/types"import { Button } from "@/components/ui/button"import { ChevronRight, ChevronDown } from "lucide-react"
type Order = { id: string orderNumber: string customer: string email: string total: number status: string date: string items: OrderItem[]}
// Expanded content componentfunction OrderDetails({ order }: { order: Order }) { return ( <div className="bg-muted/30 p-6"> <h3 className="font-semibold">Order Items</h3> <div className="space-y-2"> {order.items.map(item => ( <div key={item.id} className="flex justify-between"> <span>{item.productName}</span> <span>${item.price.toFixed(2)}</span> </div> ))} </div> </div> )}
const columns: DataTableColumnDef<Order>[] = [ { id: "expand", header: () => null, cell: ({ row }) => { if (!row.getCanExpand()) return null return ( <Button variant="ghost" size="sm" onClick={row.getToggleExpandedHandler()} className="h-6 w-6 p-0" > {row.getIsExpanded() ? ( <ChevronDown className="h-3.5 w-3.5" /> ) : ( <ChevronRight className="h-3.5 w-3.5" /> )} </Button> ) }, size: 50, enableSorting: false, enableHiding: false, meta: { expandedContent: (order: Order) => <OrderDetails order={order} />, }, }, { accessorKey: "orderNumber", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Order #" /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), }, { accessorKey: "customer", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Customer" /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), }, { accessorKey: "total", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Total" /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), cell: ({ row }) => { const total = row.getValue("total") as number return <div className="font-mono">${total.toFixed(2)}</div> }, }, { accessorKey: "status", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Status" /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), },]
export function RowExpansionTable({ data }: { data: Order[] }) { return ( <DataTableRoot data={data} columns={columns} config={{ enableExpanding: true, }} getRowCanExpand={row => row.original.items.length > 0} > <DataTableToolbarSection> <DataTableSearchFilter placeholder="Search orders..." /> <DataTableViewMenu /> </DataTableToolbarSection>
<DataTable> <DataTableHeader /> <DataTableBody> <DataTableEmptyBody /> </DataTableBody> </DataTable>
<DataTablePagination /> </DataTableRoot> )}How Row Expansion Works
Section titled “How Row Expansion Works”Row expansion is enabled by:
- Adding a column with
id: "expand"or settingmeta.expandedContenton any column - Setting
enableExpanding: truein the config - Providing
expandedContentin the column’smetaproperty
Expanded Content
Section titled “Expanded Content”The expanded content is defined in the column’s meta.expandedContent:
{ id: "expand", meta: { expandedContent: (order: Order) => <OrderDetails order={order} />, },}Conditional Expansion
Section titled “Conditional Expansion”Control which rows can be expanded:
<DataTableRoot data={data} columns={columns} config={{ enableExpanding: true, }} getRowCanExpand={row => row.original.items.length > 0}> {/* ... */}</DataTableRoot>Controlled Expansion State
Section titled “Controlled Expansion State”Full control over which rows are expanded:
import { useState } from "react"import type { ExpandedState } from "@tanstack/react-table"
export function ControlledExpansionTable({ data }: { data: Order[] }) { const [expanded, setExpanded] = useState<ExpandedState>({})
return ( <DataTableRoot data={data} columns={columns} config={{ enableExpanding: true, }} state={{ expanded, }} onExpandedChange={setExpanded} > <DataTable> <DataTableHeader /> <DataTableBody> <DataTableEmptyBody /> </DataTableBody> </DataTable> </DataTableRoot> )}Exporting Data
Section titled “Exporting Data”You can export table data to CSV format. When exporting row expansion tables, keep in mind:
- Expanded content is not exported - Only the main row data is included in the CSV
- Exclude the expand column - The expand column should be excluded from exports as it’s just a UI control
- Export respects filters - Only visible/filtered rows are exported
Using DataTableExportButton
Section titled “Using DataTableExportButton”The simplest way to add export functionality:
import { DataTableExportButton } from "@/components/niko-table/components/data-table-export-button"
export function RowExpansionTableWithExport({ data }: { data: Order[] }) { return ( <DataTableRoot data={data} columns={columns} config={{ enableExpanding: true, }} getRowCanExpand={row => row.original.items.length > 0} > <DataTableToolbarSection> <DataTableSearchFilter placeholder="Search orders..." /> <DataTableExportButton filename="orders" excludeColumns={["expand"] as unknown as (keyof Order)[]} /> <DataTableViewMenu /> </DataTableToolbarSection>
<DataTable> <DataTableHeader /> <DataTableBody /> <DataTableEmptyBody /> </DataTable>
<DataTablePagination /> </DataTableRoot> )}Using exportTableToCSV Directly
Section titled “Using exportTableToCSV Directly”For more control, use the exportTableToCSV function directly:
import { useDataTable } from "@/components/niko-table/core/data-table-context"import { exportTableToCSV } from "@/components/niko-table/filters/table-export-button"import { Button } from "@/components/ui/button"import { Download } from "lucide-react"
function ExportButton() { const { table } = useDataTable<Order>()
const handleExport = () => { exportTableToCSV(table, { filename: "orders", excludeColumns: ["expand"] as unknown as (keyof Order)[], }) }
return ( <Button size="sm" variant="outline" onClick={handleExport}> <Download className="mr-2 h-4 w-4" /> Export CSV </Button> )}
export function RowExpansionTableWithCustomExport({ data }: { data: Order[] }) { return ( <DataTableRoot data={data} columns={columns} config={{ enableExpanding: true, }} getRowCanExpand={row => row.original.items.length > 0} > <DataTableToolbarSection> <DataTableSearchFilter placeholder="Search orders..." /> <ExportButton /> <DataTableViewMenu /> </DataTableToolbarSection>
<DataTable> <DataTableHeader /> <DataTableBody /> <DataTableEmptyBody /> </DataTable>
<DataTablePagination /> </DataTableRoot> )}Exporting Only Selected Rows
Section titled “Exporting Only Selected Rows”If you have row selection enabled, you can export only the selected rows:
function ExportSelectedButton() { const { table } = useDataTable<Order>()
const handleExport = () => { exportTableToCSV(table, { filename: "selected-orders", excludeColumns: ["expand", "select"] as unknown as (keyof Order)[], onlySelected: true, }) }
const selectedCount = table.getFilteredSelectedRowModel().rows.length
if (selectedCount === 0) return null
return ( <Button size="sm" variant="outline" onClick={handleExport}> <Download className="mr-2 h-4 w-4" /> Export Selected ({selectedCount}) </Button> )}Important Notes
Section titled “Important Notes”-
Expanded content is not included: The CSV export only includes the main row data. Expanded content (like order items) is not exported. If you need to export nested data, you’ll need to flatten it into additional columns or create a custom export function.
-
Always exclude the expand column: The expand column (
id: "expand") should be excluded from exports as it’s purely a UI control:
excludeColumns={["expand"] as unknown as (keyof Order)[]}- Export respects current filters: The export function respects all active filters, sorting, and pagination. Only the currently visible/filtered rows are exported.
When to Use
Section titled “When to Use”✅ Use Row Expansion Table when:
- You need to show additional details for specific rows
- You want to keep users in context (no navigation)
- The expanded content is directly related to the row
- You prefer inline expansion over sidebars
❌ Consider other options when:
- You need to show details for all rows (use Aside Table)
- Expanded content is very large (use Aside Table)
- You don’t need additional details (use Basic Table)
Next Steps
Section titled “Next Steps”- Aside Table - Show details in sidebars
- Tree Table - Hierarchical data with expansion