Skip to content

Aside Table

Add sidebar panels to display additional content alongside your table.

Open in
"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&apos;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
Open in
"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&apos;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} (&gt;$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>
)
}

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.

Install the DataTable core and add-ons for this example:

pnpm dlx shadcn@latest add @niko-table/data-table @niko-table/data-table-aside @niko-table/data-table-pagination @niko-table/data-table-search-filter @niko-table/data-table-view-menu @niko-table/data-table-column-sort

This example also uses scroll-area from Shadcn UI:

pnpm dlx shadcn@latest add scroll-area

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.

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",
},
// ...
]

Let’s start by building a table with left and right sidebars.

First, we’ll define our columns.

columns.tsx
"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" },
},
]

Next, we’ll add the sidebar components.

aside-table.tsx
"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>
)
}

The main sidebar container component.

Props:

  • side: "left" | "right" - Which side to show the sidebar
  • open: boolean - Controlled open state
  • onOpenChange: (open: boolean) => void - Callback when open state changes
  • children: Sidebar content components

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>

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>

Use onRowClick on DataTableBody to open the right sidebar:

<DataTableBody
onRowClick={row => {
setSelectedCustomer(row)
setShowFilters(false) // Close left sidebar when opening right
}}
>
<DataTableEmptyBody />
</DataTableBody>

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>
)
}

✅ 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: