Aside Table
Add sidebar panels to display additional content alongside your table.
"use client"
import * as React from "react"import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTablePagination, DataTableSearchFilter, DataTableViewMenu, DataTableEmptyFilteredMessage, DataTableEmptyTitle, DataTableEmptyDescription, DataTableEmptyIcon, DataTableEmptyMessage, DataTableAside, DataTableAsideTrigger, DataTableAsideContent, DataTableAsideHeader, DataTableAsideTitle, DataTableAsideClose, DataTableColumnTitle, DataTableColumnHeader, DataTableColumnSortMenu,} from "@/components/niko-table/components"import { FILTER_VARIANTS } from "@/components/niko-table/lib"import { useDataTable } from "@/components/niko-table/core"import type { DataTableColumnDef } from "@/components/niko-table/types"import { Button } from "@/components/ui/button"import { Badge } from "@/components/ui/badge"import { ScrollArea } from "@/components/ui/scroll-area"import { Filter, Eye, UserSearch, SearchX } from "lucide-react"import { Checkbox } from "@/components/ui/checkbox"
type Customer = { id: string name: string email: string company: string phone: string}
const data: Customer[] = [ { id: "1", name: "John Doe", company: "Acme Corp", phone: "555-0100", }, { id: "2", name: "Jane Smith", company: "TechCo", phone: "555-0101", }, { id: "3", name: "Bob Johnson", company: "StartUp Inc", phone: "555-0102", }, { id: "4", name: "Alice Williams", company: "DesignCo", phone: "555-0103", }, { id: "5", name: "Charlie Brown", company: "Consulting LLC", phone: "555-0104", }, { id: "6", name: "Diana Prince", company: "Enterprise Inc", phone: "555-0105", }, { id: "7", name: "Ethan Hunt", company: "Mission Impossible", phone: "555-0106", }, { id: "8", name: "Fiona Green", company: "GreenTech", phone: "555-0107", }, { id: "9", name: "George Miller", company: "Media Corp", phone: "555-0108", }, { id: "10", name: "Hannah Lee", company: "Innovation Labs", phone: "555-0109", },]
function AsideFilters({ data }: { data: Customer[] }) { const { table } = useDataTable<Customer>() const column = table.getColumn("company") const filterValue = (column?.getFilterValue() as string[]) || []
const toggleCompany = (company: string, checked: boolean) => { if (checked) { column?.setFilterValue([...filterValue, company]) } else { column?.setFilterValue(filterValue.filter(v => v !== company)) } }
const companies = React.useMemo( () => Array.from(new Set(data.map(c => c.company))).sort(), [data], )
return ( <div className="mt-4 space-y-4"> <h4 className="text-sm font-medium">Company</h4> <ScrollArea className="h-[400px]"> <div className="space-y-4 pr-4"> {companies.map(company => ( <div key={company} className="flex items-center space-x-2"> <Checkbox id={`company-${company}`} checked={filterValue.includes(company)} onCheckedChange={checked => toggleCompany(company, checked === true) } /> <label htmlFor={`company-${company}`} className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" > {company} </label> </div> ))} </div> </ScrollArea> </div> )}
export default function AsideTableExample() { const [selectedCustomer, setSelectedCustomer] = React.useState<Customer | null>(null) const [showFilters, setShowFilters] = React.useState(false)
// Define columns inside component to access setSelectedCustomer const columns: DataTableColumnDef<Customer>[] = React.useMemo( () => [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Name", }, cell: ({ row }) => ( <div className="font-medium">{row.getValue("name")}</div> ), }, { accessorKey: "email", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnHeader> ), meta: { label: "Email", }, }, { accessorKey: "company", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnHeader> ), meta: { label: "Company", }, }, { accessorKey: "phone", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnHeader> ), meta: { label: "Phone", }, }, { id: "actions", header: () => <div className="text-right">Actions</div>, cell: ({ row }) => ( <div className="flex justify-end"> <Button variant="ghost" size="sm" onClick={e => { e.stopPropagation() // Prevent row click setSelectedCustomer(row.original) setShowFilters(false) // Close filters when customer is selected }} > <Eye className="mr-2 h-4 w-4" /> View </Button> </div> ), meta: { label: "Actions", }, }, ], [], )
return ( <DataTableRoot data={data} columns={columns}> <DataTableToolbarSection className="justify-between"> <div className="flex items-center gap-2"> {/* Left Sidebar - Filters with Trigger (Controlled) */} <DataTableAside side="left" open={showFilters} onOpenChange={open => { setShowFilters(open) if (open) setSelectedCustomer(null) // Close right sidebar when filters open }} > <DataTableAsideTrigger asChild> <Button variant="outline" size="sm"> <Filter className="mr-2 h-4 w-4" /> Filters </Button> </DataTableAsideTrigger> </DataTableAside> <DataTableSearchFilter placeholder="Search anything..." /> </div> <DataTableViewMenu /> </DataTableToolbarSection>
{/* Pure Composition Layout with Sidebars */} <div className="flex min-h-[600px] gap-4"> {/* Left Sidebar - Filters Content (Controlled) */} <DataTableAside side="left" open={showFilters} onOpenChange={open => { setShowFilters(open) if (open) setSelectedCustomer(null) // Close right sidebar when filters open }} > <DataTableAsideContent width="w-80"> <DataTableAsideHeader> <div className="flex items-center justify-between"> <DataTableAsideTitle>Filters</DataTableAsideTitle> <DataTableAsideClose /> </div> </DataTableAsideHeader> <AsideFilters data={data} /> </DataTableAsideContent> </DataTableAside>
{/* Main Table Area */} <DataTable className="flex-1" height="100%"> <DataTableHeader /> <DataTableBody<Customer> onRowClick={row => { setSelectedCustomer(row) setShowFilters(false) // Close filters when customer is selected }} > <DataTableEmptyBody> <DataTableEmptyMessage> <DataTableEmptyIcon> <UserSearch className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No customers found</DataTableEmptyTitle> <DataTableEmptyDescription> There are no customers to display at this time. </DataTableEmptyDescription> </DataTableEmptyMessage> <DataTableEmptyFilteredMessage> <DataTableEmptyIcon> <SearchX className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No matches found</DataTableEmptyTitle> <DataTableEmptyDescription> Try adjusting your filters or search to find what you're looking for. </DataTableEmptyDescription> </DataTableEmptyFilteredMessage> </DataTableEmptyBody> </DataTableBody> </DataTable>
{/* Right Sidebar - Customer Details (Controlled) */} <DataTableAside side="right" open={!!selectedCustomer} onOpenChange={open => { if (!open) { setSelectedCustomer(null) } else { setShowFilters(false) // Close filters when customer sidebar opens } }} > <DataTableAsideContent width="w-96"> {selectedCustomer && ( <> <DataTableAsideHeader> <div className="flex items-center justify-between"> <DataTableAsideTitle> {selectedCustomer.name} </DataTableAsideTitle> <DataTableAsideClose /> </div> <Badge className="mt-2 w-fit"> Customer ID: {selectedCustomer.id} </Badge> </DataTableAsideHeader> <ScrollArea className="mt-4 h-[500px]"> <div className="space-y-3 pr-4"> <div className="flex flex-col gap-1"> <span className="text-sm font-medium text-muted-foreground"> Email </span> <span className="text-sm">{selectedCustomer.email}</span> </div> <div className="flex flex-col gap-1"> <span className="text-sm font-medium text-muted-foreground"> Company </span> <span className="text-sm"> {selectedCustomer.company} </span> </div> <div className="flex flex-col gap-1"> <span className="text-sm font-medium text-muted-foreground"> Phone </span> <span className="text-sm">{selectedCustomer.phone}</span> </div> </div> </ScrollArea> </> )} </DataTableAsideContent> </DataTableAside> </div>
<DataTablePagination pageSizeOptions={[5, 10, 20]} /> </DataTableRoot> )}Preview with Controlled State
View Full State Object
{
"totalCustomers": 10,
"statusCounts": {
"active": 6,
"pending": 2,
"inactive": 2
},
"companies": 10,
"totalRevenue": 152900,
"totalOrders": 391,
"averageRevenue": 15290,
"highValueCount": 3
}null
[]
{
"pageIndex": 0,
"pageSize": 8
}{}"use client"
import React, { useState } from "react"import type { PaginationState, SortingState, ColumnFiltersState, VisibilityState, ColumnPinningState,} from "@tanstack/react-table"import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody,} from "@/components/niko-table/core"import { DataTableViewMenu, DataTablePagination, DataTableToolbarSection, DataTableSearchFilter, DataTableEmptyFilteredMessage, DataTableEmptyTitle, DataTableEmptyDescription, DataTableEmptyIcon, DataTableEmptyMessage,} from "@/components/niko-table/components"import { DataTableColumnHeader, DataTableColumnTitle, DataTableColumnActions, DataTableColumnSortMenu, DataTableColumnSortOptions, DataTableColumnFilter, DataTableColumnFilterTrigger, DataTableAside, DataTableAsideTrigger, DataTableAsideContent, DataTableAsideHeader, DataTableAsideClose, DataTableAsideTitle, DataTableFacetedFilter,} from "@/components/niko-table/components"import { FILTER_VARIANTS } from "@/components/niko-table/lib"import type { DataTableColumnDef } from "@/components/niko-table/types"import { Button } from "@/components/ui/button"import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card"import { Badge } from "@/components/ui/badge"import { ScrollArea } from "@/components/ui/scroll-area"import { Filter, UserSearch, SearchX } from "lucide-react"import { Checkbox } from "@/components/ui/checkbox"
type Customer = { id: string name: string company: string email: string status: "active" | "inactive" | "pending" orders: number revenue: number category?: string brand?: string}
const categoryOptions = [ { value: "electronics", label: "Electronics" }, { value: "clothing", label: "Clothing" }, { value: "home_goods", label: "Home Goods" }, { value: "books", label: "Books" },]
const brandOptions = [ { value: "brand_a", label: "Brand A" }, { value: "brand_b", label: "Brand B" }, { value: "brand_c", label: "Brand C" }, { value: "brand_d", label: "Brand D" },]
const columns: DataTableColumnDef<Customer>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader className="justify-start"> <span className="mr-2 text-sm font-semibold">Product Name</span> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Product Name", }, enableColumnFilter: true, }, { accessorKey: "category", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnActions> <div className="border-b p-2"> <DataTableFacetedFilter accessorKey="category" options={categoryOptions} trigger={ <Button variant="ghost" size="sm" className="h-8 w-full justify-start font-normal" > <Filter className="mr-2 h-4 w-4" /> Filter </Button> } /> </div> <DataTableColumnSortOptions variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Category", options: categoryOptions, }, cell: ({ row }) => { // Custom cell to match select options const category = row.getValue("category") as string const option = categoryOptions.find(opt => opt.value === category) return <span>{option?.label || category}</span> }, enableColumnFilter: true, }, { accessorKey: "brand", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnActions> <DataTableColumnFilter> <DataTableFacetedFilter accessorKey="brand" options={brandOptions} trigger={<DataTableColumnFilterTrigger />} /> </DataTableColumnFilter> <DataTableColumnSortOptions variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Brand", options: brandOptions, }, enableColumnFilter: true, }, { accessorKey: "company", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Company" /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnHeader> ), }, { accessorKey: "status", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Status" /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnHeader> ), cell: ({ row }) => { const status = row.getValue("status") as string return ( <Badge variant={ status === "active" ? "default" : status === "pending" ? "secondary" : "outline" } > {status} </Badge> ) }, }, { accessorKey: "orders", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Orders" /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.NUMBER} /> </DataTableColumnHeader> ), }, { accessorKey: "revenue", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Revenue" /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.NUMBER} /> </DataTableColumnHeader> ), cell: ({ row }) => { const revenue = parseFloat(row.getValue("revenue")) return <div className="font-medium">${revenue.toLocaleString()}</div> }, },]
const data: Customer[] = [ { id: "1", name: "John Smith", company: "Acme Corp", status: "active", orders: 42, revenue: 15600, }, { id: "2", name: "Sarah Johnson", company: "Tech Solutions Inc", status: "active", orders: 28, revenue: 12300, }, { id: "3", name: "Mike Brown", company: "Digital Ventures", status: "pending", orders: 15, revenue: 8900, }, { id: "4", name: "Lisa Davis", company: "Innovation Labs", status: "active", orders: 67, revenue: 23400, }, { id: "5", name: "Robert Wilson", company: "Future Systems", status: "inactive", orders: 3, revenue: 1200, }, { id: "6", name: "Emily Chen", company: "Cloud Dynamics", status: "active", orders: 89, revenue: 34500, }, { id: "7", name: "David Garcia", company: "Smart Analytics", status: "pending", orders: 12, revenue: 5600, }, { id: "8", name: "Jennifer Lee", company: "DataFlow Pro", status: "active", orders: 55, revenue: 19800, }, { id: "9", name: "Mark Taylor", company: "NextGen Solutions", status: "inactive", orders: 7, revenue: 2900, }, { id: "10", name: "Amanda White", company: "Quantum Corp", status: "active", orders: 73, revenue: 28700, },]
export default function AsideTableStateExample() { // Controlled state management for all table state const [globalFilter, setGlobalFilter] = useState<string | object>("") const [sorting, setSorting] = useState<SortingState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}) const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 8, }) const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({ left: [], right: [], })
// Selected customer for aside panel const [selectedCustomerId, setSelectedCustomerId] = useState<string | null>() const [showFilters, setShowFilters] = useState(false)
const selectedCustomer = selectedCustomerId ? data.find(customer => customer.id === selectedCustomerId) : null
const resetAllState = () => { setGlobalFilter("") setSorting([]) setColumnFilters([]) setColumnVisibility({}) setColumnPinning({ left: [], right: [] }) setPagination({ pageIndex: 0, pageSize: 8 }) setSelectedCustomerId(null) }
// Calculate customer metrics const customerMetrics = React.useMemo(() => { const statusCounts = data.reduce( (acc, customer) => { acc[customer.status] = (acc[customer.status] || 0) + 1 return acc }, {} as Record<string, number>, )
const companies = Array.from( new Set(data.map(customer => customer.company)), ) const totalRevenue = data.reduce( (sum, customer) => sum + customer.revenue, 0, ) const totalOrders = data.reduce((sum, customer) => sum + customer.orders, 0) const averageRevenue = totalRevenue / data.length const averageOrders = totalOrders / data.length const topCustomers = [...data] .sort((a, b) => b.revenue - a.revenue) .slice(0, 3) const highValueCustomers = data.filter(customer => customer.revenue > 20000)
return { totalCustomers: data.length, statusCounts, companies: companies.length, totalRevenue, totalOrders, averageRevenue, averageOrders, topCustomers, highValueCount: highValueCustomers.length, companyList: companies, } }, [])
return ( <div className="w-full space-y-4"> <DataTableRoot data={data} columns={columns} state={{ globalFilter, sorting, columnFilters, columnVisibility, columnPinning, pagination, }} onGlobalFilterChange={setGlobalFilter} onSortingChange={setSorting} onColumnFiltersChange={setColumnFilters} onColumnVisibilityChange={setColumnVisibility} onColumnPinningChange={setColumnPinning} onPaginationChange={setPagination} > <DataTableToolbarSection className="justify-between"> <div className="flex items-center gap-2"> {/* Left Sidebar - Filters with Trigger (Controlled) */} <DataTableAside side="left" open={showFilters} onOpenChange={open => { setShowFilters(open) if (open) setSelectedCustomerId(null) // Close right sidebar when filters open }} > <DataTableAsideTrigger asChild> <Button variant="outline" size="sm"> <Filter className="mr-2 h-4 w-4" /> Filters </Button> </DataTableAsideTrigger> </DataTableAside> <DataTableSearchFilter placeholder="Search anything..." /> </div> <DataTableViewMenu /> </DataTableToolbarSection>
{/* Pure Composition Layout with Sidebars */} <div className="flex min-h-[600px] gap-4"> {/* Left Sidebar - Filters Content (Controlled) */} <DataTableAside side="left" open={showFilters} onOpenChange={open => { setShowFilters(open) if (open) setSelectedCustomerId(null) // Close right sidebar when filters open }} > <DataTableAsideContent width="w-80"> <DataTableAsideHeader> <div className="flex items-center justify-between"> <DataTableAsideTitle>Filters</DataTableAsideTitle> <DataTableAsideClose /> </div> </DataTableAsideHeader> <div className="mt-4 space-y-4"> <h4 className="text-sm font-medium">Company</h4> <ScrollArea className="h-[400px]"> <div className="space-y-4 pr-4"> {customerMetrics.companyList.map(company => { const isChecked = columnFilters.some( f => f.id === "company" && Array.isArray(f.value) && f.value.includes(company), )
return ( <div key={company} className="flex items-center space-x-2" > <Checkbox id={`company-state-${company}`} checked={isChecked} onCheckedChange={checked => { const existing = columnFilters.find( f => f.id === "company", ) const currentValues = (existing?.value as string[]) || []
let newValues: string[] if (checked) { newValues = [...currentValues, company] } else { newValues = currentValues.filter( v => v !== company, ) }
if (newValues.length === 0) { setColumnFilters( columnFilters.filter(f => f.id !== "company"), ) } else { const newFilter = { id: "company", value: newValues, } setColumnFilters([ ...columnFilters.filter( f => f.id !== "company", ), newFilter, ]) } }} /> <label htmlFor={`company-state-${company}`} className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" > {company} </label> </div> ) })} </div> </ScrollArea> </div> </DataTableAsideContent> </DataTableAside>
{/* Main Table Area */} <DataTable className="flex-1" height="100%"> <DataTableHeader /> <DataTableBody<Customer> onRowClick={customer => { setSelectedCustomerId(customer.id) setShowFilters(false) // Close filters when customer is selected }} > <DataTableEmptyBody> <DataTableEmptyMessage> <DataTableEmptyIcon> <UserSearch className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No customers found</DataTableEmptyTitle> <DataTableEmptyDescription> There are no customers to display at this time. </DataTableEmptyDescription> </DataTableEmptyMessage> <DataTableEmptyFilteredMessage> <DataTableEmptyIcon> <SearchX className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No matches found</DataTableEmptyTitle> <DataTableEmptyDescription> Try adjusting your filters or search to find what you're looking for. </DataTableEmptyDescription> </DataTableEmptyFilteredMessage> </DataTableEmptyBody> </DataTableBody> </DataTable>
{/* Right Sidebar - Customer Details (Controlled) */} <DataTableAside side="right" open={!!selectedCustomer} onOpenChange={open => { if (!open) { setSelectedCustomerId(null) } else { setShowFilters(false) // Close filters when customer sidebar opens } }} > <DataTableAsideContent width="w-96"> {selectedCustomer && ( <> <DataTableAsideHeader> <div className="flex items-center justify-between"> <DataTableAsideTitle> {selectedCustomer.name} </DataTableAsideTitle> <DataTableAsideClose /> </div> <Badge variant={ selectedCustomer.status === "active" ? "default" : selectedCustomer.status === "pending" ? "secondary" : "outline" } className="mt-2 w-fit" > {selectedCustomer.status} </Badge> </DataTableAsideHeader> <ScrollArea className="mt-4 h-[500px]"> <div className="space-y-3 pr-4"> <div className="flex flex-col gap-1"> <span className="text-sm font-medium text-muted-foreground"> Email </span> <span className="text-sm"> {selectedCustomer.email} </span> </div> <div className="flex flex-col gap-1"> <span className="text-sm font-medium text-muted-foreground"> Company </span> <span className="text-sm"> {selectedCustomer.company} </span> </div> <div className="flex flex-col gap-1"> <span className="text-sm font-medium text-muted-foreground"> Orders </span> <span className="text-sm"> {selectedCustomer.orders} </span> </div> <div className="flex flex-col gap-1"> <span className="text-sm font-medium text-muted-foreground"> Revenue </span> <span className="text-sm"> ${selectedCustomer.revenue.toLocaleString()} </span> </div> <div className="flex flex-col gap-1"> <span className="text-sm font-medium text-muted-foreground"> Average Order Value </span> <span className="text-sm"> $ {Math.round( selectedCustomer.revenue / selectedCustomer.orders, ).toLocaleString()} </span> </div> </div> </ScrollArea> </> )} </DataTableAsideContent> </DataTableAside> </div>
<DataTablePagination pageSizeOptions={[5, 8, 10, 20]} /> </DataTableRoot>
{/* State Display for demonstration */} <Card> <CardHeader> <CardTitle>Aside Table State</CardTitle> <CardDescription> Live view of the aside table state with customer management and detailed sidebar </CardDescription> <CardAction> <Button variant="outline" size="sm" onClick={resetAllState}> Reset All State </Button> </CardAction> </CardHeader> <CardContent className="space-y-4"> <div className="grid gap-2 text-xs text-muted-foreground"> <div className="flex justify-between"> <span className="font-medium">Total Customers:</span> <span className="text-foreground"> {customerMetrics.totalCustomers} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Active:</span> <span className="text-foreground"> {customerMetrics.statusCounts.active || 0} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Pending:</span> <span className="text-foreground"> {customerMetrics.statusCounts.pending || 0} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Inactive:</span> <span className="text-foreground"> {customerMetrics.statusCounts.inactive || 0} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Companies:</span> <span className="text-foreground"> {customerMetrics.companies} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Total Revenue:</span> <span className="text-foreground"> ${customerMetrics.totalRevenue.toLocaleString()} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Total Orders:</span> <span className="text-foreground"> {customerMetrics.totalOrders.toLocaleString()} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Average Revenue:</span> <span className="text-foreground"> ${Math.round(customerMetrics.averageRevenue).toLocaleString()} </span> </div>
<div className="flex justify-between"> <span className="font-medium">High Value Customers:</span> <span className="text-foreground"> {customerMetrics.highValueCount} (>$20k) </span> </div>
<div className="flex justify-between"> <span className="font-medium">Selected Customer:</span> <span className="text-foreground"> {selectedCustomer ? selectedCustomer.name : "None"} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Sorting:</span> <span className="text-foreground"> {sorting.length > 0 ? sorting .map(s => `${s.id} ${s.desc ? "desc" : "asc"}`) .join(", ") : "None"} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Page:</span> <span className="text-foreground"> {pagination.pageIndex + 1} (Size: {pagination.pageSize}) </span> </div>
<div className="flex justify-between"> <span className="font-medium">Hidden Columns:</span> <span className="text-foreground"> { Object.values(columnVisibility).filter(v => v === false) .length } </span> </div> </div>
{/* Top Customers List */} <div className="border-t pt-4"> <div className="mb-2 text-xs font-medium"> Top 3 Customers by Revenue: </div> <div className="space-y-1"> {customerMetrics.topCustomers.map((customer, index) => ( <div key={customer.id} className="flex justify-between text-xs"> <span> {index + 1}. {customer.name} </span> <span className="font-medium"> ${customer.revenue.toLocaleString()} </span> </div> ))} </div> </div>
{/* Status Distribution */} <div className="border-t pt-4"> <div className="mb-2 text-xs font-medium">Status Distribution:</div> <div className="flex gap-2"> {Object.entries(customerMetrics.statusCounts).map( ([status, count]) => ( <div key={status} className="text-xs"> <Badge variant={ status === "active" ? "default" : status === "pending" ? "secondary" : "outline" } className="text-xs" > {status}: {count} </Badge> </div> ), )} </div> </div>
{/* Detailed state (collapsible) */} <details className="border-t pt-4"> <summary className="cursor-pointer text-xs font-medium hover:text-foreground"> View Full State Object </summary> <div className="mt-4 space-y-3 text-xs"> <div> <strong>Customer Metrics:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify( { totalCustomers: customerMetrics.totalCustomers, statusCounts: customerMetrics.statusCounts, companies: customerMetrics.companies, totalRevenue: customerMetrics.totalRevenue, totalOrders: customerMetrics.totalOrders, averageRevenue: customerMetrics.averageRevenue, highValueCount: customerMetrics.highValueCount, }, null, 2, )} </pre> </div> <div> <strong>Selected Customer:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(selectedCustomer, null, 2)} </pre> </div> <div> <strong>Sorting:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(sorting, null, 2)} </pre> </div> <div> <strong>Pagination:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(pagination, null, 2)} </pre> </div> <div> <strong>Column Visibility:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(columnVisibility, null, 2)} </pre> </div> </div> </details> </CardContent> </Card> </div> )}Introduction
Section titled “Introduction”The Aside Table adds sidebar panels (left and/or right) to display additional content alongside your table. This is useful for showing filters, details, or other contextual information without cluttering the main table view.
Installation
Section titled “Installation”- Add the required components:
npx shadcn@latest add table input button dropdown-menu scroll-area- Add
tanstack/react-tabledependency:
npm install @tanstack/react-table- Copy the DataTable components into your project. See the Installation Guide for detailed instructions.
Prerequisites
Section titled “Prerequisites”We are going to build a table with sidebars to show customer details. Here’s what our data looks like:
type Customer = { id: string name: string email: string company: string phone: string}
const data: Customer[] = [ { id: "1", name: "John Doe", company: "Acme Corp", phone: "555-0100", }, // ...]Basic Table with Sidebars
Section titled “Basic Table with Sidebars”Let’s start by building a table with left and right sidebars.
Column Definitions
Section titled “Column Definitions”First, we’ll define our columns.
"use client"
import { DataTableColumnHeader, DataTableColumnTitle, DataTableColumnSortMenu,} from "@/components/niko-table/components"import type { DataTableColumnDef } from "@/components/niko-table/types"
export type Customer = { id: string name: string email: string company: string phone: string}
export const columns: DataTableColumnDef<Customer>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Name" }, }, { accessorKey: "email", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Email" }, }, { accessorKey: "company", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Company" }, }, { accessorKey: "phone", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Phone" }, },]<DataTable /> component with Sidebars
Section titled “<DataTable /> component with Sidebars”Next, we’ll add the sidebar components.
"use client"
import { useState } from "react"import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTablePagination, DataTableSearchFilter, DataTableViewMenu, DataTableColumnHeader, DataTableColumnTitle, DataTableColumnSortMenu, DataTableAside, DataTableAsideTrigger, DataTableAsideContent, DataTableAsideHeader, DataTableAsideTitle, DataTableAsideClose,} from "@/components/niko-table/components"import type { DataTableColumnDef } from "@/components/niko-table/types"import { Button } from "@/components/ui/button"import { Badge } from "@/components/ui/badge"import { ScrollArea } from "@/components/ui/scroll-area"import { Filter, Eye } from "lucide-react"
type Customer = { id: string name: string email: string company: string phone: string}
const columns: DataTableColumnDef<Customer>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Name" }, }, { accessorKey: "email", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Email" }, }, { accessorKey: "company", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Company" }, }, { accessorKey: "phone", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Phone" }, }, { id: "actions", header: () => <div className="text-right">Actions</div>, cell: ({ row }) => ( <div className="flex justify-end"> <Button variant="ghost" size="sm" onClick={e => { e.stopPropagation() // Handle view action }} > <Eye className="mr-2 h-4 w-4" /> View </Button> </div> ), },]
export function AsideTable({ data }: { data: Customer[] }) { const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>( null, ) const [showFilters, setShowFilters] = useState(false)
return ( <DataTableRoot data={data} columns={columns}> <DataTableToolbarSection className="justify-between"> <div className="flex items-center gap-2"> {/* Left Sidebar Trigger */} <DataTableAside side="left" open={showFilters} onOpenChange={setShowFilters} > <DataTableAsideTrigger asChild> <Button variant="outline" size="sm"> <Filter className="mr-2 h-4 w-4" /> Filters </Button> </DataTableAsideTrigger> </DataTableAside> <DataTableSearchFilter placeholder="Search customers..." /> </div> <DataTableViewMenu /> </DataTableToolbarSection>
{/* Layout with Sidebars */} <div className="flex min-h-[600px] gap-4"> {/* Left Sidebar - Filters */} <DataTableAside side="left" open={showFilters} onOpenChange={setShowFilters} > <DataTableAsideContent width="w-80"> <DataTableAsideHeader> <div className="flex items-center justify-between"> <DataTableAsideTitle>Filters</DataTableAsideTitle> <DataTableAsideClose /> </div> </DataTableAsideHeader> <div className="mt-4 space-y-4"> <h4 className="text-sm font-medium">Company</h4> <ScrollArea className="h-[400px]"> <div className="space-y-2 pr-4"> {Array.from(new Set(data.map(c => c.company))).map( company => ( <label key={company} className="flex cursor-pointer items-center space-x-2" > <input type="checkbox" className="rounded" /> <span className="text-sm">{company}</span> </label> ), )} </div> </ScrollArea> </div> </DataTableAsideContent> </DataTableAside>
{/* Main Table */} <DataTable className="flex-1"> <DataTableHeader /> <DataTableBody onRowClick={row => { setSelectedCustomer(row) setShowFilters(false) }} > <DataTableEmptyBody /> </DataTableBody> </DataTable>
{/* Right Sidebar - Customer Details */} <DataTableAside side="right" open={!!selectedCustomer} onOpenChange={open => { if (!open) setSelectedCustomer(null) }} > <DataTableAsideContent width="w-96"> {selectedCustomer && ( <> <DataTableAsideHeader> <div className="flex items-center justify-between"> <DataTableAsideTitle> {selectedCustomer.name} </DataTableAsideTitle> <DataTableAsideClose /> </div> <Badge className="mt-2 w-fit"> Customer ID: {selectedCustomer.id} </Badge> </DataTableAsideHeader> <ScrollArea className="mt-4 h-[500px]"> <div className="space-y-3 pr-4"> <div className="flex flex-col gap-1"> <span className="text-sm font-medium text-muted-foreground"> Email </span> <span className="text-sm">{selectedCustomer.email}</span> </div> <div className="flex flex-col gap-1"> <span className="text-sm font-medium text-muted-foreground"> Company </span> <span className="text-sm"> {selectedCustomer.company} </span> </div> <div className="flex flex-col gap-1"> <span className="text-sm font-medium text-muted-foreground"> Phone </span> <span className="text-sm">{selectedCustomer.phone}</span> </div> </div> </ScrollArea> </> )} </DataTableAsideContent> </DataTableAside> </div>
<DataTablePagination /> </DataTableRoot> )}Sidebar Components
Section titled “Sidebar Components”DataTableAside
Section titled “DataTableAside”The main sidebar container component.
Props:
side:"left" | "right"- Which side to show the sidebaropen:boolean- Controlled open stateonOpenChange:(open: boolean) => void- Callback when open state changeschildren: Sidebar content components
DataTableAsideTrigger
Section titled “DataTableAsideTrigger”Button to trigger opening the sidebar.
<DataTableAside side="left" open={showFilters} onOpenChange={setShowFilters}> <DataTableAsideTrigger asChild> <Button variant="outline" size="sm"> <Filter className="mr-2 h-4 w-4" /> Filters </Button> </DataTableAsideTrigger></DataTableAside>DataTableAsideContent
Section titled “DataTableAsideContent”Container for sidebar content.
Props:
width?: Tailwind width class (e.g.,"w-80","w-96")children: Sidebar content
DataTableAsideHeader, DataTableAsideTitle, DataTableAsideClose
Section titled “DataTableAsideHeader, DataTableAsideTitle, DataTableAsideClose”Header components for the sidebar.
<DataTableAsideHeader> <div className="flex items-center justify-between"> <DataTableAsideTitle>Filters</DataTableAsideTitle> <DataTableAsideClose /> </div></DataTableAsideHeader>Row Click Handler
Section titled “Row Click Handler”Use onRowClick on DataTableBody to open the right sidebar:
<DataTableBody onRowClick={row => { setSelectedCustomer(row) setShowFilters(false) // Close left sidebar when opening right }}> <DataTableEmptyBody /></DataTableBody>Controlled State
Section titled “Controlled State”Manage sidebar state externally for full control:
import { useState } from "react"import type { SortingState, ColumnFiltersState } from "@tanstack/react-table"
export function ControlledAsideTable({ data }: { data: Customer[] }) { const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>( null, ) const [showFilters, setShowFilters] = useState(false) const [sorting, setSorting] = useState<SortingState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
return ( <DataTableRoot data={data} columns={columns} state={{ sorting, columnFilters, }} onSortingChange={setSorting} onColumnFiltersChange={setColumnFilters} > <DataTableToolbarSection className="justify-between"> <div className="flex items-center gap-2"> <DataTableAside side="left" open={showFilters} onOpenChange={setShowFilters} > <DataTableAsideTrigger asChild> <Button variant="outline" size="sm"> <Filter className="mr-2 h-4 w-4" /> Filters </Button> </DataTableAsideTrigger> </DataTableAside> <DataTableSearchFilter placeholder="Search customers..." /> </div> <DataTableViewMenu /> </DataTableToolbarSection>
<div className="flex min-h-[600px] gap-4"> <DataTableAside side="left" open={showFilters} onOpenChange={setShowFilters} > <DataTableAsideContent width="w-80"> {/* Filter content */} </DataTableAsideContent> </DataTableAside>
<DataTable className="flex-1"> <DataTableHeader /> <DataTableBody onRowClick={row => { setSelectedCustomer(row) setShowFilters(false) }} > <DataTableEmptyBody /> </DataTableBody> </DataTable>
<DataTableAside side="right" open={!!selectedCustomer} onOpenChange={open => { if (!open) setSelectedCustomer(null) }} > <DataTableAsideContent width="w-96"> {/* Customer details */} </DataTableAsideContent> </DataTableAside> </div>
<DataTablePagination /> </DataTableRoot> )}When to Use
Section titled “When to Use”✅ Use Aside Table when:
- You need to show additional details without cluttering the table
- You want to display filters in a dedicated panel
- You need contextual information that’s not part of the main table
- You want a clean, organized layout
❌ Consider other options when:
- You don’t need additional panels (use Basic Table)
- You prefer inline expansion (use Row Expansion Table)
Next Steps
Section titled “Next Steps”- Row Expansion Table - Expand rows inline
- Advanced Table - Combine all features