Skip to content

DataTable

A composable, themeable and customizable data table component.

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

A data table with sorting, filtering, pagination, row selection, and more.

Data tables are one of the most complex components to build. They are central to any application and often contain a lot of moving parts.

I don’t like building data tables. So I built 30+ of them. All kinds of configurations. Then I extracted the core components into data-table.

We now have a solid foundation to build on top of. Composable. Themeable. Customizable.

Browse Examples.

  1. Configure the @niko-table registry

    Add the registry to your components.json:

    components.json
    {
    "registries": {
    "@niko-table": "https://niko-table.com/r/{name}.json"
    }
    }
  2. Install the DataTable core and all add-ons

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

    For optional add-ons (pagination, filters, virtualization, etc.) and manual copy-paste, see the Installation Guide.

A DataTable component is composed of the following parts:

  • DataTableRoot - The root provider that manages table state and context.
  • DataTableToolbarSection - Container for filters, search, and actions.
  • DataTable - The table container component.
  • DataTableHeader - The table header with sortable columns.
  • DataTableBody - The table body with rows.
  • DataTablePagination - Pagination controls.
┌─────────────────────────────────────────────────────────────────┐
│ DataTableRoot │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ DataTableToolbarSection │ │
│ │ ┌─────────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ SearchFilter │ │ FilterMenu │ │ SortMenu/View │ │ │
│ │ └─────────────────┘ └─────────────┘ └─────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ DataTable │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ DataTableHeader (sticky) │ │ │
│ │ │ ┌─────────┬─────────┬─────────┬─────────────────┐ │ │ │
│ │ │ │ Column │ Column │ Column │ Column │ │ │ │
│ │ │ └─────────┴─────────┴─────────┴─────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ DataTableBody (scrollable) │ │ │
│ │ │ ┌─────────┬─────────┬─────────┬─────────────────┐ │ │ │
│ │ │ │ Cell │ Cell │ Cell │ Cell │ │ │ │
│ │ │ ├─────────┼─────────┼─────────┼─────────────────┤ │ │ │
│ │ │ │ Cell │ Cell │ Cell │ Cell │ │ │ │
│ │ │ └─────────┴─────────┴─────────┴─────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ DataTableSkeleton (when loading) │ │ │
│ │ │ DataTableEmptyBody (when no data) │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ DataTablePagination │ │
│ │ ┌─────────────┐ ┌───────────────────┐ ┌─────────────┐ │ │
│ │ │ Page Size │ │ Page 1 of 10 │ │ Navigation │ │ │
│ │ └─────────────┘ └───────────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
components/users-table.tsx
import { DataTableRoot } from "@/components/niko-table/core/data-table-root"
import { DataTable } from "@/components/niko-table/core/data-table"
import {
DataTableHeader,
DataTableBody,
DataTableEmptyBody,
DataTableSkeleton,
} from "@/components/niko-table/core/data-table-structure"
import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"
import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"
import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"
import type { DataTableColumnDef } from "@/components/niko-table/types"
type User = {
id: string
name: string
email: string
}
const columns: DataTableColumnDef<User>[] = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "email", header: "Email" },
]
export function UsersTable({ data }: { data: User[] }) {
return (
<DataTableRoot data={data} columns={columns}>
<DataTableToolbarSection>
<DataTableSearchFilter placeholder="Search users..." />
</DataTableToolbarSection>
<DataTable>
<DataTableHeader />
<DataTableBody>
<DataTableSkeleton />
<DataTableEmptyBody />
</DataTableBody>
</DataTable>
<DataTablePagination />
</DataTableRoot>
)
}

Let’s start with the most basic table. A simple table with data.

  1. Create your column definitions

    columns.tsx
    import type { DataTableColumnDef } from "@/components/niko-table/types"
    type User = {
    id: string
    name: string
    email: string
    }
    export const columns: DataTableColumnDef<User>[] = [
    {
    accessorKey: "name",
    header: "Name",
    },
    {
    accessorKey: "email",
    header: "Email",
    },
    ]
  2. Create your table component

    users-table.tsx
    import { DataTableRoot } from "@/components/niko-table/core/data-table-root"
    import { DataTable } from "@/components/niko-table/core/data-table"
    import {
    DataTableHeader,
    DataTableBody,
    } from "@/components/niko-table/core/data-table-structure"
    import { columns } from "./columns"
    export function UsersTable({ data }: { data: User[] }) {
    return (
    <DataTableRoot data={data} columns={columns}>
    <DataTable>
    <DataTableHeader />
    <DataTableBody />
    </DataTable>
    </DataTableRoot>
    )
    }
  3. Add loading and empty states

    users-table.tsx
    import { DataTableRoot } from "@/components/niko-table/core/data-table-root"
    import { DataTable } from "@/components/niko-table/core/data-table"
    import {
    DataTableHeader,
    DataTableBody,
    DataTableSkeleton,
    DataTableEmptyBody,
    } from "@/components/niko-table/core/data-table-structure"
    export function UsersTable({
    data,
    isLoading,
    }: {
    data: User[]
    isLoading?: boolean
    }) {
    return (
    <DataTableRoot data={data} columns={columns} isLoading={isLoading}>
    <DataTable>
    <DataTableHeader />
    <DataTableBody>
    <DataTableSkeleton />
    <DataTableEmptyBody />
    </DataTableBody>
    </DataTable>
    </DataTableRoot>
    )
    }
  4. Add search and pagination

    users-table.tsx
    import { DataTableRoot } from "@/components/niko-table/core/data-table-root"
    import { DataTable } from "@/components/niko-table/core/data-table"
    import {
    DataTableHeader,
    DataTableBody,
    DataTableSkeleton,
    DataTableEmptyBody,
    } from "@/components/niko-table/core/data-table-structure"
    import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"
    import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"
    import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"
    export function UsersTable({
    data,
    isLoading,
    }: {
    data: User[]
    isLoading?: boolean
    }) {
    return (
    <DataTableRoot data={data} columns={columns} isLoading={isLoading}>
    <DataTableToolbarSection>
    <DataTableSearchFilter placeholder="Search users..." />
    </DataTableToolbarSection>
    <DataTable>
    <DataTableHeader />
    <DataTableBody>
    <DataTableSkeleton />
    <DataTableEmptyBody />
    </DataTableBody>
    </DataTable>
    <DataTablePagination />
    </DataTableRoot>
    )
    }
  5. You’ve created your first table!

    Your table now has search functionality and pagination. See the Examples for more advanced configurations.

The components in data-table are built to be composable i.e you build your table by putting the provided components together. They also compose well with other shadcn/ui components such as DropdownMenu, Popover or Dialog etc.

If you need to change the code in data-table, you are encouraged to do so. The code is yours. Use data-table as a starting point and build your own.

See the Components page for detailed documentation on each component.

The DataTableRoot component is used to provide the table context to all child components. You should always wrap your table in a DataTableRoot component.

NameTypeDescription
childrenReact.ReactNodeChild components (required).
dataTData[]The data array to display in the table.
columnsDataTableColumnDef<TData>[]Column definitions array.
tableTable<TData>Pre-configured TanStack Table instance (optional).
configDataTableConfigConfiguration object for feature toggles.
statePartial<TableState>Controlled table state (pagination, sorting, etc).
isLoadingbooleanLoading state for the table.
getRowId(row: TData, index: number) => stringCustom function to get row IDs.
classNamestringAdditional CSS classes.
onGlobalFilterChange(value: GlobalFilter) => voidCallback when global filter changes.
onPaginationChange(updater: Updater<PaginationState>) => voidCallback when pagination changes.
onSortingChange(updater: Updater<SortingState>) => voidCallback when sorting changes.
onColumnFiltersChange(updater: Updater<ColumnFiltersState>) => voidCallback when column filters change.
onColumnVisibilityChange(updater: Updater<VisibilityState>) => voidCallback when column visibility changes.
onRowSelectionChange(updater: Updater<RowSelectionState>) => voidCallback when row selection changes.
onExpandedChange(updater: Updater<ExpandedState>) => voidCallback when expanded state changes.
onRowSelection(selectedRows: TData[]) => voidCallback with selected row data.

The config prop accepts a DataTableConfig object:

NameTypeDefaultDescription
enablePaginationbooleantrueEnable pagination.
enableFiltersbooleantrueEnable filtering.
enableSortingbooleantrueEnable sorting.
enableRowSelectionbooleanfalseEnable row selection.
enableMultiSortbooleantrueEnable multi-column sorting.
enableGroupingbooleanfalseEnable column grouping.
enableExpandingbooleanfalseEnable row expansion.
manualSortingbooleanfalseEnable server-side sorting.
manualPaginationbooleanfalseEnable server-side pagination.
manualFilteringbooleanfalseEnable server-side filtering.
pageCountnumber-Total pages (for server-side pagination).
initialPageSizenumber10Initial page size.
initialPageIndexnumber0Initial page index.
autoResetPageIndexboolean-Auto-reset page on filter/sort. Defaults to false when manualPagination: true.
autoResetExpandedbooleantrueAuto-reset expanded rows on filter/sort change.

The useDataTable hook is used to access the table instance from any child component.

import { useDataTable } from "@/components/niko-table/core/data-table-context"
export function CustomComponent() {
const { table, isLoading } = useDataTable()
return (
<div>
<p>Total rows: {table.getFilteredRowModel().rows.length}</p>
<p>Loading: {isLoading ? "Yes" : "No"}</p>
</div>
)
}
PropertyTypeDescription
tableDataTableInstance<TData>The TanStack Table instance.
columnsDataTableColumnDef<TData>[]The column definitions.
isLoadingbooleanWhether the table is in a loading state.
setIsLoading(isLoading: boolean) => voidProgrammatically set the loading state.

Use the state callbacks to control the table externally:

import { useState } from "react"
import type { SortingState, PaginationState } from "@tanstack/react-table"
export function ControlledTable({ data }: { data: User[] }) {
const [sorting, setSorting] = useState<SortingState>([])
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
return (
<DataTableRoot
data={data}
columns={columns}
state={{ sorting, pagination }}
onSortingChange={setSorting}
onPaginationChange={setPagination}
>
{/* ... */}
</DataTableRoot>
)
}

For server-side pagination, sorting, and filtering:

export function ServerSideTable() {
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 })
const { data, totalCount, isLoading } = useQuery({
queryKey: ["users", pagination],
queryFn: () => fetchUsers(pagination),
})
const pageCount = Math.ceil(totalCount / pagination.pageSize)
return (
<DataTableRoot
data={data ?? []}
columns={columns}
config={{
manualPagination: true,
pageCount,
}}
isLoading={isLoading}
onPaginationChange={setPagination}
>
<DataTable>
<DataTableHeader />
<DataTableBody>
<DataTableSkeleton />
<DataTableEmptyBody />
</DataTableBody>
</DataTable>
<DataTablePagination totalCount={totalCount} />
</DataTableRoot>
)
}
function UsersTable() {
const { data, isLoading } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
})
return (
<DataTableRoot data={data ?? []} columns={columns} isLoading={isLoading}>
<DataTable>
<DataTableHeader />
<DataTableBody>
<DataTableSkeleton />
<DataTableEmptyBody />
</DataTableBody>
</DataTable>
</DataTableRoot>
)
}
function UsersTable() {
const { data, isLoading } = useSWR("/api/users", fetcher)
return (
<DataTableRoot data={data ?? []} columns={columns} isLoading={isLoading}>
<DataTable>
<DataTableHeader />
<DataTableBody>
<DataTableSkeleton />
<DataTableEmptyBody />
</DataTableBody>
</DataTable>
</DataTableRoot>
)
}

Columns support metadata for advanced filtering, sorting, and display:

const columns: DataTableColumnDef<Product>[] = [
{
accessorKey: "name",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: {
label: "Product Name",
placeholder: "Search products...",
variant: "text",
},
enableColumnFilter: true,
enableSorting: true,
},
{
accessorKey: "category",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: {
label: "Category",
variant: "select",
options: [
{ label: "Electronics", value: "electronics" },
{ label: "Clothing", value: "clothing" },
],
},
enableColumnFilter: true,
},
{
accessorKey: "price",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: {
label: "Price",
variant: "range",
unit: "$",
},
enableColumnFilter: true,
},
]
VariantDescription
textText input filter.
numberNumber input filter.
selectSingle selection dropdown.
multiSelectMultiple selection dropdown.
rangeNumeric range slider.
dateSingle date picker.
dateRangeDate range picker.
booleanBoolean toggle.

The components in data-table are built to be composable. You build your table by putting the provided components together. They also compose well with other shadcn/ui components.

If you need to change the code, you are encouraged to do so. The code is yours.

The Niko Table components are organized into logical directories following the file structure in src/components/niko-table:

  • core/ - Essential table components (DataTableRoot, DataTable, context, structure components)
  • components/ - User-facing context-aware components (automatically connect to table context via useDataTable hook)
  • filters/ - Core filter implementation components (accept table prop directly, used by components/)
  • hooks/ - Custom React hooks for table functionality
  • lib/ - Utility functions and constants
  • types/ - TypeScript type definitions
  • config/ - Configuration and feature detection

This documentation follows this structure for easy navigation. Each component section includes links to source code and relevant documentation.

Essential building blocks of the data table. They handle table initialization, context management, and basic structure.

Components:

  • DataTableRoot - Provides table context and initializes TanStack Table
  • DataTable - Main table container with scrolling behavior
  • DataTableHeader - Table header with sortable columns
  • DataTableBody - Table body with rows and scroll events
  • DataTableSkeleton - Loading skeleton
  • DataTableEmptyBody - Empty state component
  • DataTableLoading - Loading indicator
  • DataTableErrorBoundary - Error boundary for table
  • Virtualized components for large datasets

Context-aware components that automatically connect to the table via the useDataTable hook. These are the recommended components for most use cases.

Components:

  • DataTableToolbarSection - Container for filters and actions
  • DataTablePagination - Full-featured pagination controls
  • DataTableSearchFilter - Global search input with debouncing
  • DataTableFilterMenu - Command palette-style filter interface
  • DataTableFacetedFilter - Faceted filter for single/multiple selection
  • DataTableSortMenu - Sort management with drag-and-drop
  • DataTableViewMenu - Column visibility toggle
  • DataTableInlineFilter - Inline filter toolbar
  • DataTableSliderFilter - Slider filter for numeric ranges
  • DataTableDateFilter - Date filter component
  • DataTableClearFilter - Clear all filters button
  • DataTableExportButton - Export to CSV button
  • DataTableColumnHeader - Sortable column header
  • DataTableColumnFacetedFilterMenu - Column-level faceted filter popover
  • DataTableColumnSliderFilterMenu - Column-level slider filter popover
  • DataTableColumnDateFilterMenu - Column-level date filter popover
  • DataTableAside - Sidebar component
  • DataTableSelectionBar - Bulk actions bar
  • DataTableEmptyState - Empty state composition components

Core filter implementation components that accept a table prop directly. They are used internally by the context-aware components but can also be used standalone when building custom components.

Components:

  • TableSearchFilter - Core search filter
  • TablePagination - Core pagination
  • TableFilterMenu - Core filter menu
  • TableFacetedFilter - Core faceted filter
  • TableSliderFilter - Core slider filter
  • TableDateFilter - Core date filter
  • TableSortMenu - Core sort menu
  • TableViewMenu - Core view menu
  • TableInlineFilter - Core inline filter
  • TableClearFilter - Core clear filter
  • TableExportButton - Core export button
  • TableRangeFilter - Core range filter

Custom React hooks for table functionality.

Hooks:

  • useDataTable - Access table instance and context
  • useDebounce - Debounce values for search/filters
  • useDerivedColumnTitle - Derive column titles
  • useGeneratedOptions - Generate filter options from data
  • useKeyboardShortcut - Manage keyboard shortcuts

Niko Table is built on top of excellent open-source projects and inspired by the work of talented developers in the community.

  • TanStack Table by Tanner Linsley - The headless table library that powers everything. Provides the foundation for all table functionality including sorting, filtering, pagination, and more.

  • Shadcn UI by Shadcn - Beautiful, accessible component primitives built on Radix UI. All UI components in Niko Table are built using Shadcn UI components.

  • sadmann7’s work - Major inspiration for filter components and table patterns:

    • TableCN - Inspired our filter menu, inline filter, faceted filter, and slider filter implementations. The composition pattern and filter architecture drew heavily from this excellent project.
    • DiceUI Sortable - Drag and drop sortable for row reordering, which inspired the sort menu implementation.
  • nuqs by François Best - Type-safe search params state manager for URL state management. Used in server-side examples for managing table state in URLs.

  • Web Dev Simplified Registry by Kyle Cook - Registry implementation pattern that inspired the structure and organization of this project.

Following the Shadcn philosophy: “Nobody’s table, everyone’s solution.”

  • Copy and paste the code into your project
  • Own the code - modify it as needed
  • No dependencies on external packages (except TanStack Table and Shadcn UI)
  • Fully customizable and themeable
  • Built with TypeScript for type safety

MIT License - use it freely in your projects!