Usage
Email | Transaction Date | Payment Reference |
---|---|---|
john@doe.com | 2021-01-01 | PAY-00123 |
jane@smith.com | 2021-01-02 | PAY-00456 |
bob@johnson.com | 2021-01-03 | PAY-00789 |
'use client'
import { createColumnHelper } from '@tanstack/react-table'
import { DataTable } from '@workspace/ui/components/DataTable'
interface Payment {
id: string
email: string
transactionDate: string
paymentReference: string
}
const columnHelper = createColumnHelper<Payment>()
export const columns = [
columnHelper.accessor('email', {
header: 'Email',
size: 270,
}),
columnHelper.accessor('transactionDate', {
header: 'Transaction Date',
}),
columnHelper.accessor('paymentReference', {
header: 'Payment Reference',
}),
]
const payments: Array<Payment> = [
{
id: '1',
email: 'john@doe.com',
transactionDate: '2021-01-01',
paymentReference: 'PAY-00123',
},
{
id: '2',
email: 'jane@smith.com',
transactionDate: '2021-01-02',
paymentReference: 'PAY-00456',
},
{
id: '3',
email: 'bob@johnson.com',
transactionDate: '2021-01-03',
paymentReference: 'PAY-00789',
},
]
export function DataTableDemo() {
return (
<div className="w-full">
<DataTable columns={columns} data={payments} />
</div>
)
}
Examples
Column Alignment
Control the alignment of column content using the meta.className
prop on column definitions.
Name | Email | Balance |
---|---|---|
John Doe | john@example.com | $1.000 |
Jane Smith | jane@example.com | $2.000 |
Bob Johnson | bob@example.com | $3.000 |
'use client'
import React from 'react'
import { createColumnHelper } from '@tanstack/react-table'
import { DataTable } from '@workspace/ui/components/DataTable'
interface User {
id: string
name: string
email: string
balance: string
}
const columnHelper = createColumnHelper<User>()
const data: User[] = [
{
id: '1',
name: 'John Doe',
email: 'john@example.com',
balance: '$1.000',
},
{
id: '2',
name: 'Jane Smith',
email: 'jane@example.com',
balance: '$2.000',
},
{
id: '3',
name: 'Bob Johnson',
email: 'bob@example.com',
balance: '$3.000',
},
]
const columns = [
columnHelper.accessor('name', {
header: 'Name',
}),
columnHelper.accessor('email', {
header: 'Email',
}),
columnHelper.accessor('balance', {
header: 'Balance',
meta: {
className: 'text-right',
},
}),
]
export function DataTableColumnAlignment() {
return (
<div className="w-full">
<DataTable columns={columns} data={data} />
</div>
)
}
Loading State
Displays a loading spinner while the table is fetching data. For optimal user experience, set a fixed table height during loading.
Product Name | Category | Price |
---|---|---|
Laptop Pro | Electronics | $1299.99 |
Wireless Mouse | Accessories | $29.99 |
Mechanical Keyboard | Accessories | $149.99 |
'use client'
import React from 'react'
import { createColumnHelper } from '@tanstack/react-table'
import { DataTable } from '@workspace/ui/components/DataTable'
import { Button } from '@workspace/ui/components/Button'
interface Product {
id: string
name: string
category: string
price: number
stock: number
}
const columnHelper = createColumnHelper<Product>()
const data: Product[] = [
{
id: '1',
name: 'Laptop Pro',
category: 'Electronics',
price: 1299.99,
stock: 15,
},
{
id: '2',
name: 'Wireless Mouse',
category: 'Accessories',
price: 29.99,
stock: 50,
},
{
id: '3',
name: 'Mechanical Keyboard',
category: 'Accessories',
price: 149.99,
stock: 25,
},
]
const columns = [
columnHelper.accessor('name', {
header: 'Product Name',
}),
columnHelper.accessor('category', {
header: 'Category',
}),
columnHelper.accessor('price', {
header: 'Price',
cell: info => `$${info.getValue().toFixed(2)}`,
meta: {
className: 'text-right',
},
}),
]
export function DataTableLoadingState() {
const [isLoading, setIsLoading] = React.useState(false)
const handleLoadData = () => {
setIsLoading(true)
setTimeout(() => {
setIsLoading(false)
}, 1000)
}
return (
<div className="w-full space-y-4">
<div className="flex gap-2">
<Button variant="outline" onClick={handleLoadData} isDisabled={isLoading}>
Simulate Initial Load
</Button>
</div>
<DataTable
columns={columns}
data={isLoading ? [] : data}
isLoading={isLoading}
containerClassName="h-[180px]"
/>
</div>
)
}
Row Selection
Enable row selection with checkboxes and implement bulk actions on selected rows.
0 of 5 selected
Name | Email | Department | |
---|---|---|---|
Alice Johnson | alice@company.com | Engineering | |
Bob Smith | bob@company.com | Marketing | |
Carol Davis | carol@company.com | Engineering | |
David Wilson | david@company.com | Sales | |
Eva Brown | eva@company.com | HR |
'use client'
import React from 'react'
import { createColumnHelper } from '@tanstack/react-table'
import { DataTable } from '@workspace/ui/components/DataTable'
import { Button } from '@workspace/ui/components/Button'
interface User {
id: string
name: string
email: string
department: string
role: string
}
const columnHelper = createColumnHelper<User>()
const data: User[] = [
{
id: '1',
name: 'Alice Johnson',
email: 'alice@company.com',
department: 'Engineering',
role: 'Senior Developer',
},
{
id: '2',
name: 'Bob Smith',
email: 'bob@company.com',
department: 'Marketing',
role: 'Marketing Manager',
},
{
id: '3',
name: 'Carol Davis',
email: 'carol@company.com',
department: 'Engineering',
role: 'Product Manager',
},
{
id: '4',
name: 'David Wilson',
email: 'david@company.com',
department: 'Sales',
role: 'Sales Representative',
},
{
id: '5',
name: 'Eva Brown',
email: 'eva@company.com',
department: 'HR',
role: 'HR Specialist',
},
]
const columns = [
columnHelper.accessor('name', {
header: 'Name',
}),
columnHelper.accessor('email', {
header: 'Email',
}),
columnHelper.accessor('department', {
header: 'Department',
}),
]
export function DataTableRowSelection() {
const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({})
const selectedCount = Object.keys(rowSelection).length
const handleBulkAction = (action: string) => {
alert(`${action} ${selectedCount} selected users`)
}
return (
<div className="w-full space-y-3">
<div className="flex items-center justify-between h-8">
<span className="text-sm text-muted-foreground">
{selectedCount} of {data.length} selected
</span>
{selectedCount > 0 && (
<div className="flex gap-2">
<Button variant="destructive" onClick={() => handleBulkAction('Delete')}>
Delete Selected
</Button>
<Button variant="outline" onClick={() => handleBulkAction('Export')}>
Export Selected
</Button>
</div>
)}
</div>
<DataTable
columns={columns}
data={data}
enableRowSelection
rowSelection={rowSelection}
setRowSelection={setRowSelection}
/>
</div>
)
}
Sorting
- Enable sorting with
enableSorting
. - Manage sort state using
sorting
andsetSorting
. - On header click, update
sorting
and send it to your server or data-fetching logic. - The table shows sort indicators, but
sorting
is fully handled server-side.
Email | Transaction Date | Payment Reference |
---|---|---|
'use client'
import { getPayments, Payment } from '@/shared/actions/examples/payments'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { createColumnHelper } from '@tanstack/react-table'
import { DataTable, type DataTableSorting } from '@workspace/ui/components/DataTable'
import { useNProgress } from '@workspace/ui/components/NProgress'
import React from 'react'
const columnHelper = createColumnHelper<Payment>()
export const columns = [
columnHelper.accessor('email', {
header: 'Email',
size: 270,
}),
columnHelper.accessor('transactionDate', {
header: 'Transaction Date',
}),
columnHelper.accessor('paymentReference', {
header: 'Payment Reference',
}),
]
export function DataTableSorting() {
const [sorting, setSorting] = React.useState<DataTableSorting | null>(null)
const payments = useQuery({
queryKey: ['payments', sorting],
queryFn: () => getPayments(sorting),
placeholderData: keepPreviousData,
})
useNProgress({
isFetching: payments.isFetching,
})
return (
<div className="w-full space-y-3">
<DataTable
enableSorting
sorting={sorting}
setSorting={setSorting}
columns={columns}
data={payments.data?.items ?? []}
isLoading={payments.isLoading}
containerClassName="h-[310px]"
/>
</div>
)
}
Column Pinning Sticky
Pin columns to the left or right to keep them visible while scrolling horizontally.
Order # | Customer | Email | Product | Qty | Price | Order Date | Total | Actions |
---|---|---|---|---|---|---|---|---|
ORD-001 | John Smith | john@example.com | Laptop Pro 15" | 1 | $1299.99 | 1/15/2024 | $1299.99 | |
ORD-002 | Sarah Johnson | sarah@example.com | Wireless Mouse | 2 | $29.99 | 1/16/2024 | $59.98 | |
ORD-003 | Mike Wilson | mike@example.com | Mechanical Keyboard | 1 | $149.99 | 1/17/2024 | $149.99 |
'use client'
import React from 'react'
import { createColumnHelper } from '@tanstack/react-table'
import { DataTable } from '@workspace/ui/components/DataTable'
import { Button } from '@workspace/ui/components/Button'
import { EditIcon, TrashIcon } from 'lucide-react'
interface Order {
id: string
orderNumber: string
customer: string
email: string
product: string
quantity: number
price: number
status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'
orderDate: string
total: number
}
const columnHelper = createColumnHelper<Order>()
const data: Order[] = [
{
id: '1',
orderNumber: 'ORD-001',
customer: 'John Smith',
email: 'john@example.com',
product: 'Laptop Pro 15"',
quantity: 1,
price: 1299.99,
status: 'delivered',
orderDate: '2024-01-15',
total: 1299.99,
},
{
id: '2',
orderNumber: 'ORD-002',
customer: 'Sarah Johnson',
email: 'sarah@example.com',
product: 'Wireless Mouse',
quantity: 2,
price: 29.99,
status: 'shipped',
orderDate: '2024-01-16',
total: 59.98,
},
{
id: '3',
orderNumber: 'ORD-003',
customer: 'Mike Wilson',
email: 'mike@example.com',
product: 'Mechanical Keyboard',
quantity: 1,
price: 149.99,
status: 'processing',
orderDate: '2024-01-17',
total: 149.99,
},
]
const columns = [
columnHelper.accessor('orderNumber', {
header: 'Order #',
size: 90,
}),
columnHelper.accessor('customer', {
header: 'Customer',
size: 150,
}),
columnHelper.accessor('email', {
header: 'Email',
size: 200,
}),
columnHelper.accessor('product', {
header: 'Product',
size: 180,
}),
columnHelper.accessor('quantity', {
header: 'Qty',
size: 80,
meta: {
className: 'text-center',
},
}),
columnHelper.accessor('price', {
header: 'Price',
size: 100,
cell: info => `$${info.getValue().toFixed(2)}`,
meta: {
className: 'text-right',
},
}),
columnHelper.accessor('orderDate', {
header: 'Order Date',
size: 120,
cell: info => new Date(info.getValue()).toLocaleDateString(),
}),
columnHelper.accessor('total', {
header: 'Total',
size: 100,
cell: info => `$${info.getValue().toFixed(2)}`,
meta: {
className: 'text-right font-medium',
},
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: () => (
<div className="space-x-1">
<Button variant="ghost" size="icon" className="h-8 w-8">
<EditIcon className="h-4 w-4 text-primary-foreground" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8">
<TrashIcon className="h-4 w-4 text-destructive-foreground" />
</Button>
</div>
),
size: 120,
enableSorting: false,
meta: {
className: 'text-center',
},
}),
]
export function DataTableSticky() {
return (
<div className="w-full">
<DataTable columns={columns} data={data} columnPinning={{ left: ['orderNumber'], right: ['actions'] }} />
</div>
)
}
Real-world Example
A complete example showing pagination, filtering, sorting, and row selection in a real application.
'use client'
import React from 'react'
import { getPayments, Payment } from '@/shared/actions/examples/payments'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { createColumnHelper } from '@tanstack/react-table'
import { Button } from '@workspace/ui/components/Button'
import { DataTable, DataTableSorting } from '@workspace/ui/components/DataTable'
import { Pagination, PaginationPageSizeSelector } from '@workspace/ui/components/Pagination'
import { BsSearchField } from '@workspace/ui/components/Searchfield'
import { BsSelect } from '@workspace/ui/components/Select'
import { CreditCardIcon, EditIcon, TrashIcon, XIcon } from 'lucide-react'
import { cn } from '@workspace/ui/lib/utils'
import { toast } from '@workspace/ui/components/Sonner'
import { confirm } from '@workspace/ui/components/ConfirmDialog'
import { useNProgress } from '@workspace/ui/components/NProgress'
const columnHelper = createColumnHelper<Payment>()
const methodOptions = [
{ id: 'debit_card', name: 'Debit Card', className: 'text-green-500 bg-green-500/10' },
{ id: 'credit_card', name: 'Credit Card', className: 'text-blue-500 bg-blue-500/10' },
{ id: 'bank_transfer', name: 'Bank Transfer', className: 'text-yellow-500 bg-yellow-500/10' },
{ id: 'paypal', name: 'Paypal', className: 'text-purple-500 bg-purple-500/10' },
]
export const columns = [
columnHelper.accessor('paymentMethod', {
header: 'Payment Method',
cell: ({ getValue }) => {
const value = getValue()
const method = methodOptions.find(option => option.id === value)
return (
<div className="flex items-center gap-2">
<div className={cn(method?.className, 'rounded-sm p-1.5')}>
<CreditCardIcon className="size-4" />
</div>
<span className="font-medium">{method?.name}</span>
</div>
)
},
}),
columnHelper.accessor('email', {
header: 'Email',
size: 270,
}),
columnHelper.accessor('transactionDate', {
header: 'Transaction Date',
}),
columnHelper.accessor('paymentReference', {
header: 'Payment Reference',
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: () => (
<div className="space-x-1">
<Button variant="ghost" size="icon" aria-label='edit'>
<EditIcon className="text-primary-foreground" />
</Button>
<Button variant="ghost" size="icon" aria-label='delete'>
<TrashIcon className="text-destructive-foreground" />
</Button>
</div>
),
size: 100,
enableSorting: false,
meta: {
className: 'text-center',
},
}),
]
export function DataTableRealworld() {
const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({
'34caaea9-44ee-4519-a6e8-4061f916d4fe': true,
})
const selectedCount = Object.keys(rowSelection).length
const [search, setSearch] = React.useState('')
const [sorting, setSorting] = React.useState<DataTableSorting | null>(null)
const [page, setPage] = React.useState(1)
const [pageSize, setPageSize] = React.useState(5)
const [paymentMethod, setPaymentMethod] = React.useState('')
const isFiltering = search || paymentMethod
const payments = useQuery({
queryKey: ['payments', sorting, page, pageSize, paymentMethod, search],
queryFn: () => getPayments({ ...sorting, page, pageSize, paymentMethod, search }),
placeholderData: keepPreviousData,
})
const handleClearFilters = () => {
setSearch('')
setPaymentMethod('')
setPage(1)
}
const handleDeleteSelected = () => {
confirm({
variant: 'destructive',
title: 'Delete Users',
description: 'Are you sure you want to delete these users?',
action: {
label: 'Delete',
onClick: () => {
toast.success({
title: `Users Deleted Successfully`,
description: `Deleted ${selectedCount} selected users`,
})
},
},
})
}
useNProgress({ isFetching: payments.isFetching })
return (
<div className="w-full space-y-3">
<div className="flex gap-2">
<BsSearchField
value={search}
onChange={value => {
setSearch(value)
setPage(1)
}}
containerClassName="max-sm:flex-1"
/>
<BsSelect
value={paymentMethod}
onChange={value => {
setPaymentMethod(String(value))
setPage(1)
}}
className="w-[155px] max-sm:hidden"
placeholder="Payment Method"
options={methodOptions}
/>
{isFiltering && (
<Button className="max-sm:hidden" variant="outline" onClick={handleClearFilters}>
<XIcon />
Clear
</Button>
)}
{!!selectedCount && (
<Button variant="destructive" className="ml-auto max-sm:hidden" onClick={handleDeleteSelected}>
Delete Selected
</Button>
)}
</div>
<DataTable
enableSorting
enableRowSelection
rowSelection={rowSelection}
setRowSelection={setRowSelection}
containerClassName="h-[335px]"
sorting={sorting}
setSorting={setSorting}
columns={columns}
data={payments.data?.items ?? []}
isLoading={payments.isLoading}
/>
<div className="flex gap-4 justify-between">
<PaginationPageSizeSelector
value={pageSize}
onChange={value => {
setPageSize(value)
setPage(1)
}}
/>
<Pagination value={page} onChange={setPage} pageCount={payments.data?.meta.totalPages ?? 1} />
</div>
</div>
)
}