Skip to content

DataTable

A composable, themeable and customizable data table component.

Open in

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. Run the following command to install the required Shadcn components

    npx shadcn@latest add table input button dropdown-menu popover command badge scroll-area separator checkbox select skeleton tooltip
  2. Add the TanStack Table dependency

    npm install @tanstack/react-table
  3. Copy the DataTable components into your project

    See the Installation Guide for detailed instructions on copying the DataTable components.

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,
DataTable,
DataTableHeader,
DataTableBody,
DataTableEmptyBody,
DataTableSkeleton,
} from "@/components/niko-table/core"
import {
DataTableToolbarSection,
DataTableSearchFilter,
DataTablePagination,
} from "@/components/niko-table/components"
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,
    DataTable,
    DataTableHeader,
    DataTableBody,
    } from "@/components/niko-table/core"
    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,
    DataTable,
    DataTableHeader,
    DataTableBody,
    DataTableSkeleton,
    DataTableEmptyBody,
    } from "@/components/niko-table/core"
    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,
    DataTable,
    DataTableHeader,
    DataTableBody,
    DataTableSkeleton,
    DataTableEmptyBody,
    } from "@/components/niko-table/core"
    import {
    DataTableToolbarSection,
    DataTableSearchFilter,
    DataTablePagination,
    } from "@/components/niko-table/components"
    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
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.
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"
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.
multi_selectMultiple selection dropdown.
rangeNumeric range slider.
dateSingle date picker.
date_rangeDate 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!