Aside Table
Add sidebar panels to display additional content alongside your table.
"use client"
import * as React from "react"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 { 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 { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"import { DataTableEmptyIcon, DataTableEmptyMessage, DataTableEmptyFilteredMessage, DataTableEmptyTitle, DataTableEmptyDescription,} from "@/components/niko-table/components/data-table-empty-state"import { DataTableAside, DataTableAsideTrigger, DataTableAsideContent, DataTableAsideHeader, DataTableAsideTitle, DataTableAsideClose,} from "@/components/niko-table/components/data-table-aside"import { DataTableViewMenu } from "@/components/niko-table/components/data-table-view-menu"import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"import { FILTER_VARIANTS } from "@/components/niko-table/lib/constants"import { useDataTable } from "@/components/niko-table/core/data-table-context"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 } 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 { DataTableColumnHeader } from "@/components/niko-table/components/data-table-column-header"import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"import { DataTableColumnActions } from "@/components/niko-table/components/data-table-column-actions"import { DataTableColumnSortMenu, DataTableColumnSortOptions,} from "@/components/niko-table/components/data-table-column-sort"import { DataTableColumnFilter } from "@/components/niko-table/components/data-table-column-filter"import { DataTableColumnFilterTrigger } from "@/components/niko-table/components/data-table-column-filter-trigger"import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"import { DataTableEmptyIcon, DataTableEmptyMessage, DataTableEmptyFilteredMessage, DataTableEmptyTitle, DataTableEmptyDescription,} from "@/components/niko-table/components/data-table-empty-state"import { DataTableAside, DataTableAsideTrigger, DataTableAsideContent, DataTableAsideHeader, DataTableAsideTitle, DataTableAsideClose,} from "@/components/niko-table/components/data-table-aside"import { DataTableViewMenu } from "@/components/niko-table/components/data-table-view-menu"import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"import { DataTableFacetedFilter } from "@/components/niko-table/components/data-table-faceted-filter"import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"import { FILTER_VARIANTS } from "@/components/niko-table/lib/constants"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”Install the DataTable core and add-ons for this example:
This example also uses scroll-area 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 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,} from "@/components/niko-table/components/data-table-column-header"import { DataTableColumnSortMenu } from "@/components/niko-table/components/data-table-column-sort"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 } 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, DataTableColumnTitle,} from "@/components/niko-table/components/data-table-column-header"import { DataTableColumnSortMenu } from "@/components/niko-table/components/data-table-column-sort"import { DataTableAside, DataTableAsideTrigger, DataTableAsideContent, DataTableAsideHeader, DataTableAsideTitle, DataTableAsideClose,} from "@/components/niko-table/components/data-table-aside"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