Data Table
A simplified wrapper for Shadcn Data Table with flexible option handling and automatic type conversion
| Status | ||||||||
|---|---|---|---|---|---|---|---|---|
EMP-1000 | John Doe | john.doe@company.com | Engineering | Manager | Active | $71,045.00 | 2023-04-24 | New York |
EMP-1001 | Jane Smith | jane.smith@company.com | Marketing | Senior | Inactive | $117,263.00 | 2022-03-05 | San Francisco |
EMP-1002 | Mike Johnson | mike.johnson@company.com | Sales | Junior | Pending | $60,040.00 | 2023-05-23 | London |
EMP-1003 | Sarah Williams | sarah.williams@company.com | HR | Lead | On Leave | $64,254.00 | 2023-11-21 | Tokyo |
EMP-1004 | David Brown | david.brown@company.com | Finance | Intern | Active | $91,545.00 | 2024-05-26 | Berlin |
EMP-1005 | Emily Davis | emily.davis@company.com | Engineering | Manager | Inactive | $60,626.00 | 2023-02-02 | New York |
EMP-1006 | Michael Wilson | michael.wilson@company.com | Marketing | Senior | Pending | $99,104.00 | 2021-10-04 | San Francisco |
EMP-1007 | Jessica Moore | jessica.moore@company.com | Sales | Junior | On Leave | $54,200.00 | 2022-12-02 | London |
EMP-1008 | Christopher Taylor | christopher.taylor@company.com | HR | Lead | Active | $123,507.00 | 2021-05-03 | Tokyo |
EMP-1009 | Amanda Anderson | amanda.anderson@company.com | Finance | Intern | Inactive | $71,782.00 | 2021-07-16 | Berlin |
Rows per page
Installation
npx shadcn@latest add @glrk-ui/data-tableData table with virtualization.
npx shadcn@latest add @glrk-ui/data-table @glrk-ui/data-table-virtualIf you haven't set up the prerequisites yet, check out Prerequest section.
npm install @tanstack/react-tableAdd following shadcn components: button, table, command, popover, dropdown-menu, select, badge.
Update same components from our site.
import { Column } from "@tanstack/react-table";
import { getLabel, getValue, isGroup } from "@/lib/utils";
import { MultiSelectCombobox, type multiSelectComboboxProps } from "../combobox";
interface ColumnFacetedFilterProps<TData, TValue>
extends Omit<multiSelectComboboxProps, 'options' | 'value' | 'onValueChange' | 'label'> {
column?: Column<TData, TValue>
title: React.ReactNode
options: optionsT
}
function change(option: allowedPrimitiveT | optionT, facets?: Map<any, number>) {
const value = getValue(option)
const label = getLabel(option)
return {
label: <>
{label}
{facets?.get(value) && (
<span className="ml-auto flex h-4 w-4 items-center justify-center text-xs">
{facets.get(value)}
</span>
)}</>,
value,
}
}
export function ColumnFacetedFilter<TData, TValue>({
column,
title,
options,
...props
}: ColumnFacetedFilterProps<TData, TValue>) {
const facets = column?.getFacetedUniqueValues()
const newOptions = options.map(option => {
if (isGroup(option)) {
return {
...option,
options: option.options.map(o => change(o, facets))
}
}
return change(option, facets)
})
function onSelect(selected: allowedPrimitiveT[]) {
column?.setFilterValue(selected?.length ? selected : undefined)
}
return (
<MultiSelectCombobox
options={newOptions}
value={column?.getFilterValue() as string[]}
onValueChange={onSelect}
label={typeof title === "object" ? title : <span className="font-semibold">{title}</span>}
indicatorAt="left"
contentCls="w-fit"
matchTriggerWidth={false}
{...props}
/>
)
}import { Column } from "@tanstack/react-table";
import { MultiSelectCombobox, type multiSelectComboboxProps } from "../combobox";
interface ColumndFilterProps<TData, TValue>
extends Omit<multiSelectComboboxProps, 'options' | 'value' | 'onValueChange' | 'label'> {
column?: Column<TData, TValue>
title: React.ReactNode
options: optionsT
}
export function ColumnFilter<TData, TValue>({
column,
title,
options,
...props
}: ColumndFilterProps<TData, TValue>) {
function onSelect(selected: allowedPrimitiveT[]) {
column?.setFilterValue(selected?.length ? selected : undefined)
}
return (
<MultiSelectCombobox
options={options}
value={column?.getFilterValue() as string[]}
onValueChange={onSelect}
label={typeof title === "object" ? title : <span className="font-semibold">{title}</span>}
contentCls="w-fit"
matchTriggerWidth={false}
{...props}
/>
)
}import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react";
import { Column } from "@tanstack/react-table";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface ColumnHeaderProps<TData, TValue> {
className?: string
column: Column<TData, TValue>
title: React.ReactNode
}
export function ColumnHeader<TData, TValue>({
column,
title,
className,
}: ColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) return <div className={cn(className)}>{title}</div>
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className={cn("-ml-2", className)}
>
{title}
{column.getIsSorted() === "desc" ? (
<ArrowDown />
) : column.getIsSorted() === "asc" ? (
<ArrowUp />
) : (
<ChevronsUpDown />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => column.getIsSorted() !== "asc" ? column.toggleSorting(false) : column.clearSorting()}
>
<ArrowUp className="h-3.5 w-3.5 text-muted-foreground/70" />
Asc
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => column.getIsSorted() !== "desc" ? column.toggleSorting(true) : column.clearSorting()}
>
<ArrowDown className="h-3.5 w-3.5 text-muted-foreground/70" />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeOff className="h-3.5 w-3.5 text-muted-foreground/70" />
Hide Column
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}import { ChevronsDown, ChevronsUp, ChevronsUpDown } from "lucide-react";
import { Column } from "@tanstack/react-table";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
interface ColumnHeaderProps<TData, TValue> {
className?: string
column: Column<TData, TValue>
title: React.ReactNode
}
export function ColumnSorter<TData, TValue>({
column,
title,
className,
}: ColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) return <div className={cn(className)}>{title}</div>
const sorted = column.getIsSorted()
function onSort() {
if (sorted === "asc") {
column.toggleSorting(true)
} else if (sorted === "desc") {
column.clearSorting()
} else {
column.toggleSorting(false)
}
}
return (
<Button
variant="ghost"
className={cn("-ml-2", className)}
onClick={onSort}
>
{title}
{sorted === "desc" ? (
<ChevronsDown className="ml-4 opacity-80" />
) : sorted === "asc" ? (
<ChevronsUp className="ml-4 opacity-80" />
) : (
<ChevronsUpDown className="ml-4 opacity-80" />
)}
</Button>
)
}"use client";
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { Settings2 } from "lucide-react";
import { Table } from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
interface ColumnToggleProps<TData> {
table: Table<TData>
}
export function ColumnToggle<TData>({ table }: ColumnToggleProps<TData>) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<Settings2 />
View
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[150px]">
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
<DropdownMenuSeparator />
{table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide()
)
.map((column) => (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id?.replace("_", " ")}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}"use client";
import { flexRender, Table as TanstackTable } from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface DataTableProps<TData> {
table: TanstackTable<TData>
emptyMessage?: string
className?: string
}
export function DataTable<TData>({
table,
emptyMessage = "No matching results.",
className = "",
}: DataTableProps<TData>) {
const columnCount = table?.getAllColumns()?.length
const rows = table?.getRowModel()?.rows
const hasRows = rows?.length > 0
return (
<Table className={className}>
<TableHeader>
{table?.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="hover:bg-transparent">
{headerGroup.headers.map((header) => (
<TableHead key={header.id} className="text-theme-grey-text">
{header.isPlaceholder ? null : flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{hasRows ? (
rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="text-[13px] capitalize">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow className="hover:bg-transparent">
<TableCell colSpan={columnCount} className="border-b">
<div className="dc h-32 my-4 text-sm text-center">
{emptyMessage}
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)
}import { useState } from "react";
import { CirclePlus } from "lucide-react";
import { Table } from "@tanstack/react-table";
import { DropdownCheckboxWrapper } from "../dropdown-menu-wrapper";
import { ColumnFilter } from "./column-filter";
import { Button } from "../button";
interface FilterGroupProps<TData> {
table: Table<TData>
options: {
value: string
lable: React.ReactNode
options: optionsT
}[]
indicatorAt?: indicatorAtT
}
export function FilterGroup<TData>({ table, options, indicatorAt }: FilterGroupProps<TData>) {
const [selected, setSelected] = useState<allowedPrimitiveT[]>([])
return (
<>
{
options
.filter(f => selected.includes(f.value))
.map(opt => (
<ColumnFilter
key={opt.value}
title={opt.lable}
column={table.getColumn(opt.value)}
options={opt.options}
/>
))
}
<DropdownCheckboxWrapper
checked={selected}
onCheckedChange={(val, checked) => setSelected(prev => !checked ? prev.filter(p => !p) : [...prev, val])}
options={options.map(m => ({ label: m.lable, value: m.value }))}
indicatorAt={indicatorAt}
>
<Button variant="outline">
<CirclePlus className="size-4" />
<span>Filter</span>
</Button>
</DropdownCheckboxWrapper>
</>
)
}import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
import { Table } from "@tanstack/react-table";
import { SelectWrapper } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
interface PaginationProps<TData> {
table: Table<TData>
}
export function Pagination<TData>({ table }: PaginationProps<TData>) {
return (
<div className="flex items-center justify-between flex-wrap gap-2 px-2 mt-4">
<div className="flex-1 whitespace-nowrap text-sm text-muted-foreground">
Total: {table.getFilteredRowModel().rows.length} row(s)
</div>
<div className="flex items-center gap-2">
<p className="text-xs">Rows per page</p>
<SelectWrapper
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => table.setPageSize(Number(value))}
options={[10, 20, 30, 40, 50]}
placeholder={`${table.getState().pagination.pageSize}`}
triggerCls="h-8 w-[70px]"
/>
</div>
<div className="text-xs mx-2">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<ChevronsLeft className="size-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeft className="size-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRight className="size-4" />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<ChevronsRight className="size-4" />
</Button>
</div>
</div>
)
}"use client";
import { useState } from "react";
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
interface useTableProps<TData, TValue> {
data: TData[];
columns: ColumnDef<TData, TValue>[];
}
export function useTable<TData, TValue>({ data, columns }: useTableProps<TData, TValue>) {
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [sorting, setSorting] = useState<SortingState>([])
const [rowSelection, setRowSelection] = useState({})
const [globalFilter, setGlobalFilter] = useState('')
return useReactTable({
data,
columns,
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
globalFilter,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
})
}
export default useTableexport * from "./column-faceted-filter";
export * from "./column-header";
export * from "./column-filter";
export * from "./column-sorter";
export * from "./column-toggle";
export * from "./filter-group";
export * from "./pagination";
export * from "./data-table";
export * from "./use-table";If you wish to add DataTableVirtualized
npm install @tanstack/react-virtual"use client";
import { useEffect, useRef } from "react";
import { type VirtualizerOptions, useVirtualizer } from "@tanstack/react-virtual";
import { flexRender, Table as TanstackTable } from "@tanstack/react-table";
import { Loader } from "lucide-react";
import { cn } from "@/lib/utils";
import {
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface DataTableProps<TData> {
table: TanstackTable<TData>
emptyMessage?: string
className?: string
hasNextPage?: boolean
isFetchingNextPage?: boolean
fetchNextPage?: () => void
virtualizerOptions?: Partial<Omit<VirtualizerOptions<HTMLDivElement, Element>, 'count' | 'getScrollElement'>>
}
export function DataTableVirtualized<TData>({
table,
emptyMessage = "No matching results.",
className = "",
hasNextPage = false,
isFetchingNextPage = false,
fetchNextPage = () => { },
virtualizerOptions,
}: DataTableProps<TData>) {
const rows = table.getRowModel().rows
const columnCount = table.getAllColumns().length
const hasRows = rows.length > 0
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: hasNextPage ? rows.length + 1 : rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 81,
overscan: 10,
...(virtualizerOptions ?? {}),
})
const virtualItems = virtualizer.getVirtualItems()
useEffect(() => {
const [lastItem] = [...virtualItems].reverse()
if (!lastItem) return
if (lastItem.index >= rows.length - 1 && hasNextPage && !isFetchingNextPage) {
fetchNextPage?.()
}
}, [rows.length, hasNextPage, virtualItems, isFetchingNextPage, fetchNextPage])
return (
<div
ref={parentRef}
className={cn("overflow-auto", className)}
>
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
<table className="w-full caption-bottom text-sm">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="hover:bg-transparent">
{headerGroup.headers.map((header) => (
<TableHead key={header.id} className="text-theme-grey-text">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{hasRows ? (
virtualItems.map((virtualRow, index) => {
const isLoaderRow = virtualRow.index > rows.length - 1
const row = rows[virtualRow.index]
if (!row) {
if (isLoaderRow && hasNextPage) {
return (
<TableRow
key="loader"
ref={virtualizer.measureElement}
data-index={virtualRow.index}
style={{
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start - index * virtualRow.size}px)`,
}}
>
<TableCell colSpan={columnCount}>
<div className="dc">
<Loader className="animate-spin" />
</div>
</TableCell>
</TableRow>
)
}
return null
}
return (
<TableRow
key={row.id}
ref={virtualizer.measureElement}
data-state={row?.getIsSelected?.() && "selected"}
data-index={virtualRow.index}
className="hover:bg-muted/40"
style={{
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start - (isLoaderRow ? index - 1 : index) * virtualRow.size}px)`,
}}
>
{
row?.getVisibleCells()?.map((cell) => (
<TableCell
key={cell.id}
className="text-[13px] capitalize"
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))
}
</TableRow>
)
})
) : (
<TableRow className="hover:bg-transparent">
<TableCell colSpan={columnCount} className="border-b">
<div className="dc h-32 my-4 text-sm text-center">{emptyMessage}</div>
</TableCell>
</TableRow>
)}
</TableBody>
</table>
</div>
</div>
)
}add import statement in the ui/data-table/index.tsx
export * from "./data-table-virtualized";
export * from "./column-faceted-filter";
export * from "./column-header";
export * from "./column-filter";
export * from "./column-sorter";
export * from "./column-toggle";
export * from "./filter-group";
export * from "./pagination";
export * from "./data-table";
export * from "./use-table";Usage
DataTable
import {
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { DataTable } from '@/components/ui/data-table';
function MyTable() {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return <DataTable table={table} />
}DataTableVirtualized
import {
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { DataTableVirtualized } from '@/components/ui/data-table-virtualized';
function MyTable() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery()
const table = useReactTable({
data: data.pages.flat(),
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<DataTableVirtualized
table={table}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
fetchNextPage={fetchNextPage}
className="h-[600px]" // Fixed height required
/>
)
}useTable Hook
Custom hook for managing table state and configuration. Pre-configured with columnVisibility, columnFilters (Faceted), sorting, rowSelection, globalFilter, pagination.
So you can use most of the functionality of tanstack-table just using this hook.
import { useTable } from '@/components/ui/use-table';
import { DataTable } from '@/components/ui/data-table';
function MyTable() {
const table = useTable({ data, columns })
return (
<>
//... Other components
<DataTable table={table} />
</>
)
}ColumnSorter
Simple column header with sorting functionality.
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: ({ column }) => (
<ColumnSorter column={column} title="Name" />
),
},
]ColumnHeader
Advanced column header with dropdown menu for sorting and visibility.
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: ({ column }) => (
<ColumnHeader column={column} title="Name" />
),
},
]ColumnFilter
Multi-select dropdown filter for column values.
<ColumnFilter
title="Role"
column={table.getColumn('role')}
options={['Manager', 'Senior', 'Junior', 'Lead', 'Intern']}
/>ColumnFacetedFilter
Multi-select dropdown filter with faceted value counts.
<ColumnFacetedFilter
title="Role"
column={table.getColumn('role')}
options={['Manager', 'Senior', 'Junior', 'Lead', 'Intern']}
/>FilterGroup
Dynamic filter management component that allows adding/removing filters (ColumnFilter).
<FilterGroup
table={table}
options={[
{
key: 'department',
lable: 'Department',
options: ['Engineering', 'Marketing', 'Sales']
},
{
key: 'location',
lable: 'Location',
options: ['New York', 'London', 'Tokyo']
}
]}
/>ColumnToggle
Dropdown menu for showing/hiding table columns.
<ColumnToggle table={table} />Pagination
Comprehensive pagination controls with page size selection.
<Pagination table={table} />Note: options array is of optionsT type, so you can pass allowed data. DataTable fully customizable with column array itself.
Reference
ColumndFilter and ColumnFacetedFilter
Prop
Type
ColumnHeader
Prop
Type
ColumnSorter
Prop
Type
ColumnToggle and Pagination
Prop
Type
useTable
Prop
Type
DataTable
Prop
Type
DataTableVirtualized
Prop
Type