Data Table

Powerful table and datagrids built using TanStack Table.

Usage

Email
Transaction Date
Payment Reference
john@doe.com2021-01-01PAY-00123
jane@smith.com2021-01-02PAY-00456
bob@johnson.com2021-01-03PAY-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 Doejohn@example.com$1.000
Jane Smithjane@example.com$2.000
Bob Johnsonbob@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 ProElectronics$1299.99
Wireless MouseAccessories$29.99
Mechanical KeyboardAccessories$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 Johnsonalice@company.comEngineering
Bob Smithbob@company.comMarketing
Carol Daviscarol@company.comEngineering
David Wilsondavid@company.comSales
Eva Browneva@company.comHR
'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 and setSorting.
  • 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-001John Smithjohn@example.comLaptop Pro 15"1$1299.991/15/2024$1299.99
ORD-002Sarah Johnsonsarah@example.comWireless Mouse2$29.991/16/2024$59.98
ORD-003Mike Wilsonmike@example.comMechanical Keyboard1$149.991/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.

Payment Method
Email
Transaction Date
Payment Reference
Actions
Items per page
'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>
    )
}