Skip to content

Aside Table

Add sidebar panels to display additional content alongside your table.

Mounts left and/or right panels that share the layout with the table so contextual chrome — filter trees, row detail, summary stats — doesn’t compete with the data view for horizontal space. The panels are regular slots: put whatever React you want inside.

Open in
Name
Email
Company
Phone
Actions
John Doe
john@example.comAcme Corp555-0100
Jane Smith
jane@techco.comTechCo555-0101
Bob Johnson
bob@startup.ioStartUp Inc555-0102
Alice Williams
alice@designco.comDesignCo555-0103
Charlie Brown
charlie@consulting.comConsulting LLC555-0104
Diana Prince
diana@enterprise.comEnterprise Inc555-0105
Ethan Hunt
ethan@mission.ioMission Impossible555-0106
Fiona Green
fiona@greentech.comGreenTech555-0107
George Miller
george@media.comMedia Corp555-0108
Hannah Lee
hannah@innovation.ioInnovation Labs555-0109
Preview with Controlled State
Open in
Product Name
Category
Brand
Company
Status
Orders
Revenue
John SmithAcme Corpactive42
$15,600
Sarah JohnsonTech Solutions Incactive28
$12,300
Mike BrownDigital Venturespending15
$8,900
Lisa DavisInnovation Labsactive67
$23,400
Robert WilsonFuture Systemsinactive3
$1,200
Emily ChenCloud Dynamicsactive89
$34,500
David GarciaSmart Analyticspending12
$5,600
Jennifer LeeDataFlow Proactive55
$19,800
Aside Table State
Live view of the aside table state with customer management and detailed sidebar
Total Customers:10
Active:6
Pending:2
Inactive:2
Companies:10
Total Revenue:$152,900
Total Orders:391
Average Revenue:$15,290
High Value Customers:3 (>$20k)
Selected Customer:None
Sorting:None
Page:1 (Size: 8)
Hidden Columns:0
Top 3 Customers by Revenue:
1. Emily Chen$34,500
2. Amanda White$28,700
3. Lisa Davis$23,400
Status Distribution:
active: 6
pending: 2
inactive: 2
View Full State Object
Customer Metrics:
{
  "totalCustomers": 10,
  "statusCounts": {
    "active": 6,
    "pending": 2,
    "inactive": 2
  },
  "companies": 10,
  "totalRevenue": 152900,
  "totalOrders": 391,
  "averageRevenue": 15290,
  "highValueCount": 3
}
Selected Customer:
null
Sorting:
[]
Pagination:
{
  "pageIndex": 0,
  "pageSize": 8
}
Column Visibility:
{}

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",
email: "john@example.com",
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 } from "@/components/niko-table/components/data-table-column-header"
import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"
import { DataTableColumnSortMenu } from "@/components/niko-table/components/data-table-column-sort"
import type { DataTableColumnDef } from "@/components/niko-table/types"
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 } 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 {
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: