Aside Table
Add sidebar panels to display additional content alongside your table.
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
}{}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