Skip to content

Niko Table

Build production-ready data tables with sorting, filtering, pagination, virtualization, and more.

Nobody’s table, everyone’s solution.

Just like Shadcn, Niko Table is not a component library. Instead, it’s a comprehensive DataTable component that you copy into your project and customize to your needs. Built with TanStack Table and Shadcn UI, it provides enterprise-grade features while remaining fully under your control.

npx shadcn@latest add https://niko-table.com/r/data-table.json

See ALL features in action with this comprehensive example:

Open in
"use client"
/**
* All Features Table Example
*
* This example demonstrates ALL available features of the DataTable:
* - Multi-column sorting
* - Advanced filtering (global search + column filters with AND/OR logic)
* - Pagination
* - Row selection with bulk actions
* - Column visibility
* - Row expansion
* - Sidebar panels (left for filters, right for details)
* - Data export (CSV)
* - Controlled state management
* - Selection bar with bulk actions
*/
import { useState, useCallback, useMemo } from "react"
import type {
PaginationState,
SortingState,
ColumnFiltersState,
VisibilityState,
RowSelectionState,
ExpandedState,
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 {
DataTableAside,
DataTableAsideContent,
DataTableAsideHeader,
DataTableAsideTitle,
DataTableAsideDescription,
DataTableAsideClose,
} from "@/components/niko-table/components/data-table-aside"
import { DataTableClearFilter } from "@/components/niko-table/components/data-table-clear-filter"
import { DataTableColumnActions } from "@/components/niko-table/components/data-table-column-actions"
import { DataTableColumnDateFilterOptions } from "@/components/niko-table/components/data-table-column-date-filter-options"
import { DataTableColumnFacetedFilterOptions } from "@/components/niko-table/components/data-table-column-faceted-filter"
import { DataTableColumnHeader } from "@/components/niko-table/components/data-table-column-header"
import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"
import { DataTableColumnHideOptions } from "@/components/niko-table/components/data-table-column-hide"
import { DataTableColumnPinOptions } from "@/components/niko-table/components/data-table-column-pin"
import { DataTableColumnSliderFilterOptions } from "@/components/niko-table/components/data-table-column-slider-filter-options"
import { DataTableColumnSortOptions } from "@/components/niko-table/components/data-table-column-sort"
import {
DataTableEmptyIcon,
DataTableEmptyMessage,
DataTableEmptyFilteredMessage,
DataTableEmptyTitle,
DataTableEmptyDescription,
DataTableEmptyActions,
} from "@/components/niko-table/components/data-table-empty-state"
import { DataTableFacetedFilter } from "@/components/niko-table/components/data-table-faceted-filter"
import { DataTableFilterMenu } from "@/components/niko-table/components/data-table-filter-menu"
import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"
import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"
import { DataTableSelectionBar } from "@/components/niko-table/components/data-table-selection-bar"
import { DataTableSliderFilter } from "@/components/niko-table/components/data-table-slider-filter"
import { DataTableSortMenu } from "@/components/niko-table/components/data-table-sort-menu"
import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"
import { DataTableViewMenu } from "@/components/niko-table/components/data-table-view-menu"
import {
SYSTEM_COLUMN_IDS,
FILTER_VARIANTS,
JOIN_OPERATORS,
} from "@/components/niko-table/lib/constants"
import { useDataTable } from "@/components/niko-table/core/data-table-context"
import { daysAgo } from "@/components/niko-table/lib/format"
import { exportTableToCSV } from "@/components/niko-table/filters/table-export-button"
import type {
DataTableColumnDef,
ExtendedColumnFilter,
} from "@/components/niko-table/types"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { SearchX, UserSearch } from "lucide-react"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"
import {
Download,
Trash2,
ChevronRight,
ChevronDown,
MoreHorizontal,
} from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
type Product = {
id: string
name: string
category: string
brand: string
price: number
stock: number
rating: number
inStock: boolean
releaseDate: Date
description: string
tags: string[]
}
const categoryOptions = [
{ label: "Electronics", value: "electronics" },
{ label: "Clothing", value: "clothing" },
{ label: "Home & Garden", value: "home-garden" },
{ label: "Sports", value: "sports" },
{ label: "Books", value: "books" },
]
const brandOptions = [
{ label: "Apple", value: "apple" },
{ label: "Samsung", value: "samsung" },
{ label: "Nike", value: "nike" },
{ label: "Adidas", value: "adidas" },
{ label: "Sony", value: "sony" },
{ label: "LG", value: "lg" },
{ label: "Dell", value: "dell" },
{ label: "HP", value: "hp" },
]
const initialData: Product[] = [
{
id: "1",
name: "iPhone 15 Pro",
category: "electronics",
brand: "apple",
price: 999,
stock: 45,
rating: 5,
inStock: true,
releaseDate: daysAgo(5),
description: "Latest iPhone with A17 Pro chip and titanium design",
tags: ["premium", "new", "smartphone"],
},
{
id: "2",
name: "Galaxy S24 Ultra",
category: "electronics",
brand: "samsung",
price: 1199,
stock: 32,
rating: 5,
inStock: true,
releaseDate: daysAgo(10),
description: "Flagship Android phone with S Pen and AI features",
tags: ["premium", "new", "smartphone"],
},
{
id: "3",
name: "Air Jordan 1",
category: "sports",
brand: "nike",
price: 170,
stock: 8,
rating: 4,
inStock: true,
releaseDate: daysAgo(25),
description: "Classic basketball sneakers with iconic design",
tags: ["sneakers", "basketball", "classic"],
},
{
id: "4",
name: "Ultraboost 23",
category: "sports",
brand: "adidas",
price: 190,
stock: 15,
rating: 4,
inStock: true,
releaseDate: daysAgo(50),
description: "Running shoes with Boost technology",
tags: ["running", "comfort", "athletic"],
},
{
id: "5",
name: "PlayStation 5",
category: "electronics",
brand: "sony",
price: 499,
stock: 0,
rating: 5,
inStock: false,
releaseDate: daysAgo(365),
description: "Next-gen gaming console with ray tracing",
tags: ["gaming", "console", "entertainment"],
},
{
id: "6",
name: "OLED C3 TV",
category: "electronics",
brand: "lg",
price: 1499,
stock: 12,
rating: 5,
inStock: true,
releaseDate: daysAgo(90),
description: "55-inch OLED TV with perfect blacks",
tags: ["tv", "entertainment", "premium"],
},
{
id: "7",
name: "XPS 15 Laptop",
category: "electronics",
brand: "dell",
price: 1899,
stock: 20,
rating: 4,
inStock: true,
releaseDate: daysAgo(120),
description: "Premium laptop for professionals",
tags: ["laptop", "professional", "premium"],
},
{
id: "8",
name: "Spectre x360",
category: "electronics",
brand: "hp",
price: 1599,
stock: 18,
rating: 4,
inStock: true,
releaseDate: daysAgo(15),
description: "2-in-1 convertible laptop",
tags: ["laptop", "convertible", "versatile"],
},
{
id: "9",
name: "MacBook Pro 16",
category: "electronics",
brand: "apple",
price: 2499,
stock: 25,
rating: 5,
inStock: true,
releaseDate: daysAgo(30),
description: "Powerful laptop for creative professionals",
tags: ["laptop", "professional", "creative"],
},
{
id: "10",
name: "Galaxy Book3",
category: "electronics",
brand: "samsung",
price: 1399,
stock: 14,
rating: 4,
inStock: true,
releaseDate: daysAgo(180),
description: "Sleek Windows laptop",
tags: ["laptop", "windows", "sleek"],
},
{
id: "11",
name: "Running Shorts",
category: "clothing",
brand: "nike",
price: 45,
stock: 120,
rating: 3,
inStock: true,
releaseDate: daysAgo(60),
description: "Comfortable running shorts",
tags: ["clothing", "running", "athletic"],
},
{
id: "12",
name: "Training Jacket",
category: "clothing",
brand: "adidas",
price: 85,
stock: 65,
rating: 4,
inStock: true,
releaseDate: daysAgo(45),
description: "Lightweight training jacket",
tags: ["clothing", "training", "athletic"],
},
{
id: "13",
name: "Garden Tools Set",
category: "home-garden",
brand: "hp",
price: 120,
stock: 30,
rating: 4,
inStock: true,
releaseDate: daysAgo(75),
description: "Complete set of gardening tools",
tags: ["tools", "garden", "home"],
},
{
id: "14",
name: "Programming Book",
category: "books",
brand: "dell",
price: 60,
stock: 50,
rating: 5,
inStock: true,
releaseDate: daysAgo(200),
description: "Learn React and TypeScript",
tags: ["book", "programming", "education"],
},
{
id: "15",
name: "Wireless Mouse",
category: "electronics",
brand: "lg",
price: 35,
stock: 200,
rating: 3,
inStock: true,
releaseDate: daysAgo(150),
description: "Ergonomic wireless mouse",
tags: ["accessories", "computer", "wireless"],
},
]
// Expanded row content component
function ExpandedRowContent({ product }: { product: Product }) {
return (
<div className="bg-muted/30 p-4">
<div className="space-y-3">
<div>
<h4 className="mb-2 text-sm font-semibold">Description</h4>
<p className="text-sm text-muted-foreground">{product.description}</p>
</div>
<div>
<h4 className="mb-2 text-sm font-semibold">Tags</h4>
<div className="flex flex-wrap gap-2">
{product.tags.map(tag => (
<Badge key={tag} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
</div>
</div>
</div>
</div>
)
}
// Product details component for sidebar
function ProductDetails({ product }: { product: Product }) {
return (
<ScrollArea className="h-full">
<div className="space-y-6 p-6">
<div>
<h2 className="text-2xl font-bold">{product.name}</h2>
<p className="mt-1 text-sm text-muted-foreground">
{categoryOptions.find(opt => opt.value === product.category)?.label}
</p>
</div>
<Separator />
<div className="space-y-4">
<div>
<h3 className="mb-2 text-sm font-semibold">Details</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Brand:</span>
<span>
{brandOptions.find(opt => opt.value === product.brand)?.label}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Price:</span>
<span className="font-medium">${product.price.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Stock:</span>
<span
className={
product.stock < 10 ? "font-medium text-red-600" : ""
}
>
{product.stock} units
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Rating:</span>
<div className="flex items-center gap-1">
<span>{product.rating}</span>
<span className="text-yellow-500">★</span>
</div>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Status:</span>
<Badge variant={product.inStock ? "default" : "secondary"}>
{product.inStock ? "In Stock" : "Out of Stock"}
</Badge>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Release Date:</span>
<span>{product.releaseDate.toLocaleDateString()}</span>
</div>
</div>
</div>
<Separator />
<div>
<h3 className="mb-2 text-sm font-semibold">Description</h3>
<p className="text-sm text-muted-foreground">
{product.description}
</p>
</div>
<Separator />
<div>
<h3 className="mb-2 text-sm font-semibold">Tags</h3>
<div className="flex flex-wrap gap-2">
{product.tags.map(tag => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
</div>
</div>
</div>
</div>
</ScrollArea>
)
}
// Bulk actions component
function BulkActions() {
const { table } = useDataTable<Product>()
const selectedRows = table.getFilteredSelectedRowModel().rows
const selectedCount = selectedRows.length
const handleBulkExport = () => {
exportTableToCSV(table, {
filename: "selected-products",
excludeColumns: [
"select",
"expand",
"actions",
] as unknown as (keyof Product)[],
onlySelected: true,
})
}
const handleBulkDelete = () => {
// In a real app, you would delete the selected items
console.log(
"Deleting:",
selectedRows.map(row => row.original.id),
)
table.resetRowSelection()
}
return (
<DataTableSelectionBar
selectedCount={selectedCount}
onClear={() => table.resetRowSelection()}
>
<Button size="sm" variant="outline" onClick={handleBulkExport}>
<Download className="mr-2 h-4 w-4" />
Export Selected
</Button>
<Button size="sm" variant="destructive" onClick={handleBulkDelete}>
<Trash2 className="mr-2 h-4 w-4" />
Delete Selected
</Button>
</DataTableSelectionBar>
)
}
// Filter toolbar component
function FilterToolbar({
filters,
onFiltersChange,
}: {
filters: ExtendedColumnFilter<Product>[]
onFiltersChange: (filters: ExtendedColumnFilter<Product>[] | null) => void
}) {
return (
<DataTableToolbarSection className="w-full flex-col justify-between gap-2">
<DataTableToolbarSection className="px-0">
<DataTableSearchFilter placeholder="Search products..." />
<DataTableViewMenu />
</DataTableToolbarSection>
<DataTableToolbarSection className="flex-wrap px-0">
<DataTableFacetedFilter
accessorKey="category"
title="Category"
options={categoryOptions}
multiple
/>
<DataTableFacetedFilter
accessorKey="brand"
title="Brand"
options={brandOptions}
limitToFilteredRows
multiple
/>
<DataTableSliderFilter accessorKey="price" />
<DataTableSortMenu />
<DataTableFilterMenu
filters={filters}
onFiltersChange={onFiltersChange}
/>
<DataTableClearFilter />
</DataTableToolbarSection>
</DataTableToolbarSection>
)
}
export default function AllFeaturesTableExample() {
// Controlled state management
const [data] = useState<Product[]>(initialData)
const [globalFilter, setGlobalFilter] = useState<string | object>("")
const [sorting, setSorting] = useState<SortingState>([])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [expanded, setExpanded] = useState<ExpandedState>({})
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({
left: [],
right: [],
})
// Sidebar state
const [selectedProductId, setSelectedProductId] = useState<string | null>(
null,
)
const selectedProduct = selectedProductId
? data.find(product => product.id === selectedProductId)
: null
const resetAllState = useCallback(() => {
setGlobalFilter("")
setSorting([])
setColumnFilters([])
setColumnVisibility({})
setRowSelection({})
setExpanded({})
setColumnPinning({ left: [], right: [] })
setPagination({ pageIndex: 0, pageSize: 10 })
setSelectedProductId(null)
}, [])
// Extract filters for display
const currentFilters = useMemo(() => {
if (
typeof globalFilter === "object" &&
globalFilter &&
"filters" in globalFilter
) {
const filterObj = globalFilter as {
filters: ExtendedColumnFilter<Product>[]
}
return filterObj.filters || []
}
return columnFilters
.map(cf => cf.value)
.filter(
(v): v is ExtendedColumnFilter<Product> =>
v !== null && typeof v === "object" && "id" in v,
)
}, [globalFilter, columnFilters])
// Handler for filter menu
const handleFiltersChange = useCallback(
(filters: ExtendedColumnFilter<Product>[] | null) => {
if (!filters || filters.length === 0) {
setColumnFilters([])
setGlobalFilter("")
setPagination(prev => ({ ...prev, pageIndex: 0 }))
} else {
const hasOrFilters = filters.some(
(filter, index) => index > 0 && filter.joinOperator === "or",
)
if (hasOrFilters) {
setColumnFilters([])
setGlobalFilter({
filters,
joinOperator: "mixed",
})
setPagination(prev => ({ ...prev, pageIndex: 0 }))
} else {
setGlobalFilter("")
setColumnFilters(
filters.map(filter => ({
id: filter.id,
value: filter,
})),
)
setPagination(prev => ({ ...prev, pageIndex: 0 }))
}
}
},
[],
)
// Helper to display global filter state
const getGlobalFilterDisplay = () => {
if (typeof globalFilter === "string") {
return globalFilter || "None"
}
if (
typeof globalFilter === "object" &&
globalFilter &&
"filters" in globalFilter
) {
const filterObj = globalFilter as {
filters: unknown[]
joinOperator: string
}
return `OR Filter (${filterObj.filters?.length || 0} conditions)`
}
return "None"
}
// Extract actual filter data for display
const displayFilters = useMemo(() => {
if (
typeof globalFilter === "object" &&
globalFilter &&
"filters" in globalFilter
) {
const filterObj = globalFilter as {
filters: unknown[]
joinOperator: string
}
return filterObj.filters || []
}
return columnFilters
}, [columnFilters, globalFilter])
// Enhanced filter statistics
const filterStats = useMemo(() => {
if (
typeof globalFilter === "object" &&
globalFilter &&
"filters" in globalFilter
) {
const filterObj = globalFilter as {
filters: Array<{
joinOperator?: string
value?: unknown
}>
joinOperator: string
}
const filters = filterObj.filters || []
const hasAndFilters = filters.some(
(filter, index) =>
index === 0 || filter.joinOperator === JOIN_OPERATORS.AND,
)
const hasOrFilters = filters.some(
(filter, index) =>
index > 0 && filter.joinOperator === JOIN_OPERATORS.OR,
)
return {
totalFilters: filters.length,
hasAndFilters,
hasOrFilters,
effectiveJoinOperator: hasOrFilters
? JOIN_OPERATORS.MIXED
: JOIN_OPERATORS.AND,
activeFilters: filters.filter(f => f.value && f.value !== "").length,
}
}
const hasAndFilters = columnFilters.length > 0
const hasOrFilters = columnFilters.some(
filter =>
typeof filter.value === "object" &&
filter.value &&
"joinOperator" in filter.value &&
filter.value.joinOperator === "or",
)
return {
totalFilters: columnFilters.length,
hasAndFilters,
hasOrFilters,
effectiveJoinOperator: hasOrFilters
? JOIN_OPERATORS.MIXED
: JOIN_OPERATORS.AND,
activeFilters: columnFilters.filter(f => f.value && f.value !== "")
.length,
}
}, [columnFilters, globalFilter])
// Get current filter mode
const getFilterMode = () => {
if (
typeof globalFilter === "object" &&
globalFilter &&
"filters" in globalFilter
) {
const filterObj = globalFilter as {
filters: unknown[]
joinOperator: string
}
if (filterObj.joinOperator === "mixed") {
return "MIXED"
}
return filterObj.joinOperator.toUpperCase()
}
const hasOrOperators = columnFilters.some(
filter =>
typeof filter.value === "object" &&
filter.value &&
"joinOperator" in filter.value &&
filter.value.joinOperator === "or",
)
return hasOrOperators ? "MIXED" : "AND"
}
// Define columns with all features
const columns: DataTableColumnDef<Product>[] = useMemo(
() => [
{
id: SYSTEM_COLUMN_IDS.SELECT,
size: 40, // Compact width for checkbox column
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={value => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
id: SYSTEM_COLUMN_IDS.EXPAND,
header: () => null,
cell: ({ row }) => {
if (!row.getCanExpand()) return null
return (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={row.getToggleExpandedHandler()}
>
{row.getIsExpanded() ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
)
},
size: 50,
enableSorting: false,
enableHiding: false,
meta: {
expandedContent: (product: Product) => (
<ExpandedRowContent product={product} />
),
},
},
{
accessorKey: "name",
header: () => (
<DataTableColumnHeader className="justify-start">
<DataTableColumnTitle>Product Name</DataTableColumnTitle>
<DataTableColumnActions>
<DataTableColumnSortOptions withSeparator={false} />
<DataTableColumnPinOptions />
<DataTableColumnHideOptions />
</DataTableColumnActions>
</DataTableColumnHeader>
),
meta: {
label: "Product Name",
variant: FILTER_VARIANTS.TEXT,
},
enableColumnFilter: true,
cell: ({ row }) => (
<div
className="cursor-pointer font-medium hover:underline"
onClick={() => {
setSelectedProductId(row.original.id)
}}
>
{row.getValue("name")}
</div>
),
},
{
accessorKey: "category",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
{/* Composable Actions: Multi-select filter example */}
<DataTableColumnActions label="Category Options">
<DataTableColumnSortOptions
variant={FILTER_VARIANTS.TEXT}
withSeparator={false}
/>
<DataTableColumnFacetedFilterOptions
options={categoryOptions}
multiple
/>
<DataTableColumnPinOptions />
<DataTableColumnHideOptions />
</DataTableColumnActions>
</DataTableColumnHeader>
),
meta: {
label: "Category",
variant: FILTER_VARIANTS.SELECT,
options: categoryOptions,
},
cell: ({ row }) => {
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 />
{/* Composable Actions: Single-select filter example */}
<DataTableColumnActions label="Brand Options">
<DataTableColumnSortOptions
variant={FILTER_VARIANTS.TEXT}
withSeparator={false}
/>
<DataTableColumnFacetedFilterOptions
options={brandOptions}
multiple={false}
/>
<DataTableColumnPinOptions />
<DataTableColumnHideOptions />
</DataTableColumnActions>
</DataTableColumnHeader>
),
meta: {
label: "Brand",
variant: FILTER_VARIANTS.SELECT,
options: brandOptions,
},
enableColumnFilter: true,
},
{
accessorKey: "price",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnActions>
<DataTableColumnSortOptions withSeparator={false} />
<DataTableColumnSliderFilterOptions />
<DataTableColumnPinOptions />
<DataTableColumnHideOptions />
</DataTableColumnActions>
</DataTableColumnHeader>
),
meta: {
label: "Price",
unit: "$",
variant: FILTER_VARIANTS.RANGE,
},
cell: ({ row }) => {
const price = parseFloat(row.getValue("price"))
return <div className="font-medium">${price.toFixed(2)}</div>
},
enableColumnFilter: true,
},
{
accessorKey: "stock",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
{/* All actions composed in single dropdown */}
<DataTableColumnActions>
<DataTableColumnSortOptions
variant={FILTER_VARIANTS.NUMBER}
withSeparator={false}
/>
<DataTableColumnPinOptions />
<DataTableColumnHideOptions />
</DataTableColumnActions>
</DataTableColumnHeader>
),
meta: {
label: "Stock",
variant: FILTER_VARIANTS.NUMBER,
},
cell: ({ row }) => {
const stock = Number(row.getValue("stock"))
return (
<div className={stock < 10 ? "font-medium text-red-600" : ""}>
{stock}
</div>
)
},
enableColumnFilter: true,
},
{
accessorKey: "rating",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnActions>
<DataTableColumnSortOptions
variant={FILTER_VARIANTS.NUMBER}
withSeparator={false}
/>
<DataTableColumnPinOptions />
<DataTableColumnHideOptions />
</DataTableColumnActions>
</DataTableColumnHeader>
),
meta: {
label: "Rating",
variant: FILTER_VARIANTS.NUMBER,
},
cell: ({ row }) => {
const rating = Number(row.getValue("rating"))
return (
<div className="flex items-center gap-1">
<span>{rating}</span>
<span className="text-yellow-500">★</span>
</div>
)
},
enableColumnFilter: true,
},
{
accessorKey: "inStock",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnActions>
<DataTableColumnSortOptions withSeparator={false} />
<DataTableColumnPinOptions />
<DataTableColumnHideOptions />
</DataTableColumnActions>
</DataTableColumnHeader>
),
meta: {
label: "In Stock",
variant: FILTER_VARIANTS.BOOLEAN,
},
cell: ({ row }) => {
const inStock = Boolean(row.getValue("inStock"))
return (
<Badge variant={inStock ? "default" : "secondary"}>
{inStock ? "Yes" : "No"}
</Badge>
)
},
enableColumnFilter: true,
},
{
accessorKey: "releaseDate",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnActions>
<DataTableColumnSortOptions withSeparator={false} />
<DataTableColumnDateFilterOptions />
<DataTableColumnPinOptions />
<DataTableColumnHideOptions />
</DataTableColumnActions>
</DataTableColumnHeader>
),
meta: {
label: "Release Date",
variant: FILTER_VARIANTS.DATE,
},
cell: ({ row }) => {
const date = row.getValue("releaseDate") as Date
return <span>{date.toLocaleDateString()}</span>
},
enableColumnFilter: true,
},
{
id: "actions",
header: () => <div className="text-right">Actions</div>,
cell: ({ row }) => {
const product = row.original
return (
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setSelectedProductId(product.id)
}}
>
View Details
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => console.log("Edit", product.id)}
>
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => console.log("Delete", product.id)}
className="text-red-600"
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
},
enableSorting: false,
enableHiding: false,
},
],
[],
)
return (
<div className="w-full space-y-4">
<DataTableRoot
data={data}
columns={columns}
config={{
enablePagination: true,
enableSorting: true,
enableMultiSort: true,
enableFilters: true,
enableRowSelection: true,
enableExpanding: true,
}}
getRowCanExpand={() => true}
getSubRows={() => undefined}
state={{
globalFilter,
sorting,
columnFilters,
columnVisibility,
rowSelection,
expanded,
columnPinning,
pagination,
}}
onGlobalFilterChange={value => {
setGlobalFilter(value)
setPagination(prev => ({ ...prev, pageIndex: 0 }))
}}
onSortingChange={setSorting}
onColumnFiltersChange={setColumnFilters}
onColumnVisibilityChange={setColumnVisibility}
onRowSelectionChange={setRowSelection}
onExpandedChange={setExpanded}
onColumnPinningChange={setColumnPinning}
onPaginationChange={setPagination}
>
<FilterToolbar
filters={currentFilters}
onFiltersChange={handleFiltersChange}
/>
<BulkActions />
{/* Sidebar Layout */}
<div className="flex min-h-150 gap-4">
{/* Main Table Area */}
<DataTable className="flex-1" height="100%">
<DataTableHeader />
<DataTableBody
onRowClick={(product: Product) => {
console.log("Row clicked:", product.id)
setSelectedProductId(product.id)
}}
>
<DataTableEmptyBody>
<DataTableEmptyMessage>
<DataTableEmptyIcon>
<UserSearch className="size-12" />
</DataTableEmptyIcon>
<DataTableEmptyTitle>No products found</DataTableEmptyTitle>
<DataTableEmptyDescription>
Get started by adding your first product to the inventory.
</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>
<DataTableEmptyActions>
<Button onClick={() => alert("Add product clicked")}>
Add Product
</Button>
</DataTableEmptyActions>
</DataTableEmptyBody>
</DataTableBody>
</DataTable>
{/* Right Sidebar - Product Details */}
{selectedProduct && (
<DataTableAside
side="right"
open={!!selectedProduct}
onOpenChange={open => {
if (!open) setSelectedProductId(null)
}}
>
<DataTableAsideContent width="w-78">
<DataTableAsideHeader>
<DataTableAsideTitle>Product Details</DataTableAsideTitle>
<DataTableAsideDescription>
View detailed information
</DataTableAsideDescription>
<DataTableAsideClose />
</DataTableAsideHeader>
<ProductDetails product={selectedProduct} />
</DataTableAsideContent>
</DataTableAside>
)}
</div>
<DataTablePagination />
</DataTableRoot>
{/* State Display */}
<Card>
<CardHeader>
<CardTitle>Current Table State</CardTitle>
<CardDescription>
Live view of all table state for demonstration
</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">Search Query:</span>
<span className="text-foreground">
{getGlobalFilterDisplay()}
</span>
</div>
<div className="flex justify-between">
<span className="font-medium">Total Items:</span>
<span className="text-foreground">{data.length}</span>
</div>
<div className="flex justify-between">
<span className="font-medium">Selected Rows:</span>
<span className="text-foreground">
{
Object.keys(rowSelection).filter(key => rowSelection[key])
.length
}
</span>
</div>
<div className="flex justify-between">
<span className="font-medium">Expanded Rows:</span>
<span className="text-foreground">
{typeof expanded === "object" && expanded !== null
? Object.keys(expanded).filter(
key => (expanded as Record<string, boolean>)[key],
).length
: 0}
</span>
</div>
<div className="flex justify-between">
<span className="font-medium">Active Filters:</span>
<span className="text-foreground">{columnFilters.length}</span>
</div>
<div className="flex justify-between">
<span className="font-medium">Enhanced Filters:</span>
<span className="text-foreground">
{filterStats.totalFilters}
</span>
</div>
<div className="flex justify-between">
<span className="font-medium">Active Enhanced:</span>
<span className="text-foreground">
{filterStats.activeFilters}
</span>
</div>
<div className="flex justify-between">
<span className="font-medium">Join Logic:</span>
<span className="text-foreground">
{filterStats.effectiveJoinOperator}
</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 className="flex justify-between">
<span className="font-medium">Pinned Columns:</span>
<span className="text-foreground">
{columnPinning.left?.length || 0} Left,{" "}
{columnPinning.right?.length || 0} Right
</span>
</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>Enhanced Filters:</strong>
<pre className="mt-1 overflow-auto rounded bg-muted p-2">
{displayFilters.length > 0
? JSON.stringify(displayFilters, null, 2)
: "No enhanced filters"}
</pre>
</div>
<div>
<strong>Column Pinning:</strong>
<pre className="mt-1 overflow-auto rounded bg-muted p-2">
{JSON.stringify(columnPinning, null, 2)}
</pre>
</div>
<div>
<strong>Filter Stats:</strong>
<pre className="mt-1 overflow-auto rounded bg-muted p-2">
{JSON.stringify(filterStats, null, 2)}
</pre>
</div>
<div>
<strong>Filter Mode:</strong> {getFilterMode()}
<div className="mt-1 text-muted-foreground">
{getFilterMode() === "AND"
? "All conditions must match (stored in columnFilters)"
: getFilterMode() === "OR"
? "Any condition can match (stored in globalFilter)"
: "Mixed logic - individual AND/OR operators per filter"}
</div>
</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>Column Filters State (AND logic):</strong>
<pre className="mt-1 overflow-auto rounded bg-muted p-2">
{JSON.stringify(columnFilters, null, 2)}
</pre>
</div>
<div>
<strong>Global Filter State (OR logic):</strong>
<pre className="mt-1 overflow-auto rounded bg-muted p-2">
{JSON.stringify(globalFilter, 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>
<strong>Row Selection:</strong>
<pre className="mt-1 overflow-auto rounded bg-muted p-2">
{JSON.stringify(rowSelection, null, 2)}
</pre>
</div>
<div>
<strong>Expanded Rows:</strong>
<pre className="mt-1 overflow-auto rounded bg-muted p-2">
{JSON.stringify(expanded, null, 2)}
</pre>
</div>
</div>
</details>
</CardContent>
</Card>
</div>
)
}

This demo showcases every feature:

  • Row selection - Select individual or all rows with checkboxes
  • Bulk actions - Export or delete selected rows
  • Row expansion - Click expand icon to see additional details
  • Sidebar panels - Left sidebar for quick filters, right sidebar for product details
  • Multi-column sorting - Click column headers to sort
  • Advanced filtering - Global search + column-specific filters with AND/OR logic
  • Pagination - Navigate through pages with customizable page sizes
  • Column visibility - Show/hide columns dynamically
  • Data export - Export all or selected rows to CSV
  • Controlled state - Full state management with React hooks
  • Real-time updates - See state changes reflected immediately
  • Type-safe - Full TypeScript support
  • Accessible - WCAG 2.1 compliant with keyboard navigation
  • Responsive - Mobile-friendly design
  • Customizable - Full source code access
  • Composable - Mix and match components
  • Performance - Virtual scrolling for 10,000+ rows
  • State Management - Context-based with hook flexibility

Have questions or want to contribute?

MIT License - feel free to use this in your projects!