Data Table
A simplified wrapper for Shadcn Data Table with flexible item handling and automatic type conversion
| Status | ||||||||
|---|---|---|---|---|---|---|---|---|
EMP-1000 | John Doe | john.doe@company.com | Engineering | Manager | Active | $140,695.00 | 2022-08-15 | New York |
EMP-1001 | Jane Smith | jane.smith@company.com | Marketing | Senior | Inactive | $57,409.00 | 2021-03-13 | San Francisco |
EMP-1002 | Mike Johnson | mike.johnson@company.com | Sales | Junior | Pending | $85,925.00 | 2021-04-30 | London |
EMP-1003 | Sarah Williams | sarah.williams@company.com | HR | Lead | On Leave | $104,270.00 | 2020-07-16 | Tokyo |
EMP-1004 | David Brown | david.brown@company.com | Finance | Intern | Active | $57,096.00 | 2024-03-15 | Berlin |
EMP-1005 | Emily Davis | emily.davis@company.com | Engineering | Manager | Inactive | $88,171.00 | 2022-01-19 | New York |
EMP-1006 | Michael Wilson | michael.wilson@company.com | Marketing | Senior | Pending | $114,172.00 | 2024-02-13 | San Francisco |
EMP-1007 | Jessica Moore | jessica.moore@company.com | Sales | Junior | On Leave | $85,388.00 | 2020-09-21 | London |
EMP-1008 | Christopher Taylor | christopher.taylor@company.com | HR | Lead | Active | $67,947.00 | 2020-05-04 | Tokyo |
EMP-1009 | Amanda Anderson | amanda.anderson@company.com | Finance | Intern | Inactive | $115,940.00 | 2020-11-19 | 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, popover, dropdown-menu, select, badge.
Update same components from our site.
import { useEffect, useState } from 'react'
import { type Column } from '@tanstack/react-table'
import { getLabel, getValue, isGroup } from '@/lib/utils'
import { ComboboxWrapper, type ComboboxWrapperProps } from '@/components/ui/combobox'
interface ColumnFacetedFilterProps<TData, TValue>
extends Omit<ComboboxWrapperProps, 'value' | 'onValueChange' | 'label'> {
column?: Column<TData, TValue>
title: React.ReactNode
}
function change(item: allowedPrimitiveT | itemT, facets?: Map<any, number>) {
const value = getValue(item)
const label = getLabel(item)
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,
items,
...props
}: ColumnFacetedFilterProps<TData, TValue>) {
const [facets, setFacets] = useState<Map<unknown, number> | undefined>()
useEffect(() => {
setFacets(column?.getFacetedUniqueValues())
}, [column])
const newItems = (items ?? []).map(item => {
if (isGroup(item)) {
return {
...item,
items: item.items.map(o => change(o, facets)),
}
}
return change(item, facets)
})
function onSelect(selected: allowedPrimitiveT[]) {
column?.setFilterValue(selected?.length ? selected : undefined)
}
return (
<ComboboxWrapper
multiple
items={newItems}
value={(column?.getFilterValue() as string[]) ?? []}
onValueChange={v => onSelect(v as any)}
// label={typeof title === "object" ? title : <span className="font-semibold">{title}</span>}
indicatorAt=""
{...props}
/>
)
}
import { type Column } from '@tanstack/react-table'
import { ComboboxWrapper, type ComboboxWrapperProps } from '@/components/ui/combobox'
interface ColumndFilterProps<TData, TValue>
extends Omit<ComboboxWrapperProps, 'value' | 'onValueChange' | 'label'> {
column?: Column<TData, TValue>
title: React.ReactNode
}
export function ColumnFilter<TData, TValue>({
title,
items,
column,
...props
}: ColumndFilterProps<TData, TValue>) {
function onSelect(selected: allowedPrimitiveT) {
column?.setFilterValue(selected ?? undefined)
}
return (
<ComboboxWrapper
items={items}
value={column?.getFilterValue() ?? ''}
onValueChange={v => onSelect(v as any)}
// label={typeof title === "object" ? title : <span className="font-semibold">{title}</span>}
{...props}
/>
)
}
import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from 'lucide-react'
import { type Column } from '@tanstack/react-table'
import { cn } from '@/lib/utils'
import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from '@/components/ui/menu'
import { buttonVariants } from '@/components/ui/button'
interface ColumnHeaderProps<TData, TValue> {
className?: string
column: Column<TData, TValue>
title: React.ReactNode
}
export function ColumnHeader<TData, TValue>({
title,
column,
className,
}: ColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) return <div className={cn(className)}>{title}</div>
return (
<Menu>
<MenuTrigger className={cn(buttonVariants({ variant: 'ghost', className }), '-ml-2')}>
{title}
{column.getIsSorted() === 'desc' ? (
<ArrowDown />
) : column.getIsSorted() === 'asc' ? (
<ArrowUp />
) : (
<ChevronsUpDown />
)}
</MenuTrigger>
<MenuContent align="start">
<MenuItem
onClick={() =>
column.getIsSorted() !== 'asc' ? column.toggleSorting(false) : column.clearSorting()
}
>
<ArrowUp className="h-3.5 w-3.5 text-muted-foreground/70" />
Asc
</MenuItem>
<MenuItem
onClick={() =>
column.getIsSorted() !== 'desc' ? column.toggleSorting(true) : column.clearSorting()
}
>
<ArrowDown className="h-3.5 w-3.5 text-muted-foreground/70" />
Desc
</MenuItem>
<MenuSeparator />
<MenuItem onClick={() => column.toggleVisibility(false)}>
<EyeOff className="h-3.5 w-3.5 text-muted-foreground/70" />
Hide Column
</MenuItem>
</MenuContent>
</Menu>
)
}
import { ChevronsDown, ChevronsUp, ChevronsUpDown } from 'lucide-react'
import { type 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>({
title,
column,
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>
)
}
import { Settings2 } from 'lucide-react'
import { type Table } from '@tanstack/react-table'
import { Menu, MenuCheckboxItem, MenuContent, MenuTrigger } from '@/components/ui/menu'
import { buttonVariants } from '@/components/ui/button'
interface ColumnToggleProps<TData> {
table: Table<TData>
}
export function ColumnToggle<TData>({ table }: ColumnToggleProps<TData>) {
return (
<Menu>
<MenuTrigger className={buttonVariants({ variant: 'outline' })}>
<Settings2 />
View
</MenuTrigger>
<MenuContent align="end" className="w-38">
{/* <MenuLabel>Toggle columns</MenuLabel> */}
{/* <MenuSeparator /> */}
{table
.getAllColumns()
.filter(column => typeof column.accessorFn !== 'undefined' && column.getCanHide())
.map(column => (
<MenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={value => column.toggleVisibility(!!value)}
>
{column.id?.replace('_', ' ')}
</MenuCheckboxItem>
))}
</MenuContent>
</Menu>
)
}
'use client'
import { flexRender, type Table as TanstackTable } from '@tanstack/react-table'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
interface DataTableProps<TData> {
table: TanstackTable<TData>
className?: string
emptyMessage?: string
}
export function DataTable<TData>({
table,
className = '',
emptyMessage = 'No matching results.',
}: 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 { type Table } from '@tanstack/react-table'
import { type ComboboxWrapperProps } from '@/components/ui/combobox'
import { MenuCheckboxWrapper } from '@/components/ui/menu-wrapper'
import { buttonVariants } from '@/components/ui/button'
import { ColumnFilter } from './column-filter'
type ColumnFilterPassthroughProps = Omit<
ComboboxWrapperProps,
'items' | 'value' | 'onValueChange' | 'label' | 'multiple'
>
type MenuCheckboxPassthroughProps = Omit<
React.ComponentProps<typeof MenuCheckboxWrapper>,
'trigger' | 'items' | 'checked' | 'onCheckedChange'
>
interface FilterGroupProps<TData> {
table: Table<TData>
items: {
value: string
lable: React.ReactNode
items: itemsT
columnFilterProps?: ColumnFilterPassthroughProps
}[]
columnFilterProps?: ColumnFilterPassthroughProps
menuCheckboxProps?: MenuCheckboxPassthroughProps
}
export function FilterGroup<TData>({
table,
items,
columnFilterProps,
menuCheckboxProps,
}: FilterGroupProps<TData>) {
const [selected, setSelected] = useState<allowedPrimitiveT[]>([])
return (
<>
{items
.filter(f => selected.includes(f.value))
.map(opt => (
<ColumnFilter
key={opt.value}
title={opt.lable}
items={opt.items}
column={table.getColumn(opt.value)}
{...columnFilterProps}
{...opt.columnFilterProps}
/>
))}
<MenuCheckboxWrapper
trigger={
<>
<CirclePlus className="size-4" /> Filter
</>
}
triggerCls={buttonVariants({ variant: 'outline' })}
{...menuCheckboxProps}
checked={selected}
onCheckedChange={(val, checked) =>
setSelected(prev => (!checked ? prev.filter(p => !p) : [...prev, val]))
}
items={items.map(m => ({ label: m.lable, value: m.value }))}
/>
</>
)
}
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'
import { type 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))}
items={[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 useTable
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'
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, type 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>
className?: string
hasNextPage?: boolean
emptyMessage?: string
isFetchingNextPage?: boolean
fetchNextPage?: () => void
virtualizerOptions?: Partial<
Omit<VirtualizerOptions<HTMLDivElement, Element>, 'count' | 'getScrollElement'>
>
}
export function DataTableVirtualized<TData>({
table,
className = '',
hasNextPage = false,
emptyMessage = 'No matching results.',
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';
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, 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')}
items={['Manager', 'Senior', 'Junior', 'Lead', 'Intern']}
/>ColumnFacetedFilter
Multi-select dropdown filter with faceted value counts.
<ColumnFacetedFilter
title="Role"
column={table.getColumn('role')}
items={['Manager', 'Senior', 'Junior', 'Lead', 'Intern']}
/>FilterGroup
Dynamic filter management component that allows adding/removing filters (ColumnFilter).
<FilterGroup
table={table}
items={[
{
key: 'department',
lable: 'Department',
items: ['Engineering', 'Marketing', 'Sales']
},
{
key: 'location',
lable: 'Location',
items: ['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: items array is of itemsT 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