Data Table
A powerful data table component built on Tanstack Table with support for sorting, filtering, pagination, row selection, and virtualization.
| Feature | Status |
|---|---|
| Sorting | ✅ |
| Filtering (global & column) | ✅ |
| Pagination (client & server) | ✅ |
| Row selection | ✅ |
| Virtualization | ✅ |
| Column visibility | ✅ |
| Column resizing | ✅ |
| Column pinning | ✅ |
| Column reordering | ❌ |
| Grouping | ❌ |
| Expanding | ❌ |
| Clickable rows | ✅ |
| Cell overflow handling | ✅ |
| Keyboard navigation | ✅ |
| Sticky headers | ✅ |
| Server-side data | ✅ |
| Loading states | ✅ |
| Error states | ✅ |
DataTable is built on Tanstack Table. For simple static tables without these features, use the base Table component instead.
Pagination
Client-side and server-side pagination with full examples
Selection & Actions
Row selection, bulk actions, and clickable rows
Columns & Filtering
Column visibility, resizing, pinning, and filtering
Advanced
Virtualization, loading/error states, composable API
Full-Width Playground
Interactive full-page table example
Basic Usage
DataTable uses a composable API - wrap your content with DataTableContent to render the table.
| Invoice | Status | Type | Customer | Amount | |
|---|---|---|---|---|---|
| INV-0001 | Paid | credit | John Doe | john.doe@example.com | €321.00 |
| INV-0002 | Pending | debit | Jane Smith | jane.smith@example.com | €106.00 |
| INV-0003 | Pending | debit | Bob Wilson | bob.wilson@example.com | €348.00 |
| INV-0004 | Overdue | credit | Alice Brown | alice.brown@example.com | €421.00 |
| INV-0005 | Overdue | credit | Charlie Davis | charlie.davis@example.com | €399.00 |
| INV-0006 | Pending | credit | Eva Martinez | eva.martinez@example.com | €529.00 |
| INV-0007 | Pending | debit | Frank Miller | frank.miller@example.com | €546.00 |
| INV-0008 | Pending | credit | Grace Lee | grace.lee@example.com | €299.00 |
| INV-0009 | Paid | credit | Henry Chen | henry.chen@example.com | €387.00 |
| INV-0010 | Paid | credit | Isabel Garcia | isabel.garcia@example.com | €476.00 |
import { DataTable, DataTableContent } from "@epilot/volt-ui"
import type { ColumnDef } from "@tanstack/react-table"
type Invoice = {
id: string
status: string
customer: string
amount: number
}
const columns: ColumnDef<Invoice>[] = [
{ accessorKey: "id", header: "Invoice" },
{ accessorKey: "status", header: "Status" },
{ accessorKey: "customer", header: "Customer" },
{ accessorKey: "amount", header: "Amount" },
]
// Column resizing is enabled by default. Disable if you prefer fixed columns:
<DataTable columns={columns} data={invoices} disableColumnResizing>
<DataTableContent />
</DataTable>Full Examples
Client-Side
A complete example with sorting, row selection, column filtering, column visibility, pagination, and a toolbar with search.
| Invoice | Status | Type | Customer | Amount | Actions | ||
|---|---|---|---|---|---|---|---|
| INV-0001 | Paid | credit | John Doe | john.doe@example.com | €321.00 | ||
| INV-0002 | Pending | debit | Jane Smith | jane.smith@example.com | €106.00 | ||
| INV-0003 | Pending | debit | Bob Wilson | bob.wilson@example.com | €348.00 | ||
| INV-0004 | Overdue | credit | Alice Brown | alice.brown@example.com | €421.00 | ||
| INV-0005 | Overdue | credit | Charlie Davis | charlie.davis@example.com | €399.00 | ||
| INV-0006 | Pending | credit | Eva Martinez | eva.martinez@example.com | €529.00 | ||
| INV-0007 | Pending | debit | Frank Miller | frank.miller@example.com | €546.00 | ||
| INV-0008 | Pending | credit | Grace Lee | grace.lee@example.com | €299.00 | ||
| INV-0009 | Paid | credit | Henry Chen | henry.chen@example.com | €387.00 | ||
| INV-0010 | Paid | credit | Isabel Garcia | isabel.garcia@example.com | €476.00 |
import { useState } from "react"
import type { ColumnFiltersState, SortingState } from "@tanstack/react-table"
import {
DataTable,
DataTableContent,
DataTableHeader,
DataTableBody,
DataTablePagination,
DataTableColumnVisibility,
DataTableToolbar,
Button,
} from "@epilot/volt-ui"
function FullExample() {
const [globalFilter, setGlobalFilter] = useState("")
const [rowSelection, setRowSelection] = useState({})
const [sorting, setSorting] = useState<SortingState>([])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const statusFilter = columnFilters.find((f) => f.id === "status")?.value as string | undefined
const typeFilter = columnFilters.find((f) => f.id === "type")?.value as string | undefined
const setStatusFilter = (value: string | undefined) => {
setColumnFilters((prev) =>
value
? [...prev.filter((f) => f.id !== "status"), { id: "status", value }]
: prev.filter((f) => f.id !== "status")
)
}
const setTypeFilter = (value: string | undefined) => {
setColumnFilters((prev) =>
value
? [...prev.filter((f) => f.id !== "type"), { id: "type", value }]
: prev.filter((f) => f.id !== "type")
)
}
return (
<DataTable
columns={columns}
data={data}
enableSorting
enableRowSelection
enableGlobalFilter
enableFiltering
sorting={sorting}
onSortingChange={setSorting}
globalFilter={globalFilter}
onGlobalFilterChange={setGlobalFilter}
columnFilters={columnFilters}
onColumnFiltersChange={setColumnFilters}
rowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
pagination={{ pageSize: 10 }}
>
<DataTableToolbar>
<input
type="text"
placeholder="Search..."
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
/>
<select
value={statusFilter ?? ""}
onChange={(e) => setStatusFilter(e.target.value || undefined)}
>
<option value="">All statuses</option>
<option value="paid">Paid</option>
<option value="pending">Pending</option>
</select>
<select
value={typeFilter ?? ""}
onChange={(e) => setTypeFilter(e.target.value || undefined)}
>
<option value="">All types</option>
<option value="credit">Credit</option>
<option value="debit">Debit</option>
</select>
{columnFilters.length > 0 && (
<Button variant="tertiary" size="sm" onClick={() => setColumnFilters([])}>
Clear filters
</Button>
)}
<div className="flex-1" />
<DataTableColumnVisibility label="Toggle columns" />
</DataTableToolbar>
<DataTableContent>
<DataTableHeader />
<DataTableBody />
</DataTableContent>
<DataTablePagination />
</DataTable>
)
}Server-Side
A complete server-side example with manual sorting, filtering, and pagination.
| Invoice | Status | Type | Customer | Amount | Actions | ||
|---|---|---|---|---|---|---|---|
| INV-0001 | Paid | credit | John Doe | john.doe@example.com | €321.00 | ||
| INV-0002 | Pending | debit | Jane Smith | jane.smith@example.com | €106.00 | ||
| INV-0003 | Pending | debit | Bob Wilson | bob.wilson@example.com | €348.00 | ||
| INV-0004 | Overdue | credit | Alice Brown | alice.brown@example.com | €421.00 | ||
| INV-0005 | Overdue | credit | Charlie Davis | charlie.davis@example.com | €399.00 | ||
| INV-0006 | Pending | credit | Eva Martinez | eva.martinez@example.com | €529.00 | ||
| INV-0007 | Pending | debit | Frank Miller | frank.miller@example.com | €546.00 | ||
| INV-0008 | Pending | credit | Grace Lee | grace.lee@example.com | €299.00 | ||
| INV-0009 | Paid | credit | Henry Chen | henry.chen@example.com | €387.00 | ||
| INV-0010 | Paid | credit | Isabel Garcia | isabel.garcia@example.com | €476.00 |
import { useState, useMemo } from "react"
import type { ColumnFiltersState, PaginationState, SortingState } from "@tanstack/react-table"
import {
DataTable,
DataTableContent,
DataTableHeader,
DataTableBody,
DataTablePagination,
DataTableColumnVisibility,
DataTableToolbar,
Button,
} from "@epilot/volt-ui"
function ServerExample() {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const [sorting, setSorting] = useState<SortingState>([])
const [globalFilter, setGlobalFilter] = useState("")
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [rowSelection, setRowSelection] = useState({})
const statusFilter = columnFilters.find((f) => f.id === "status")?.value as string | undefined
const typeFilter = columnFilters.find((f) => f.id === "type")?.value as string | undefined
const setStatusFilter = (value: string | undefined) => {
setColumnFilters((prev) =>
value
? [...prev.filter((f) => f.id !== "status"), { id: "status", value }]
: prev.filter((f) => f.id !== "status")
)
setPagination((prev) => ({ ...prev, pageIndex: 0 }))
}
const setTypeFilter = (value: string | undefined) => {
setColumnFilters((prev) =>
value
? [...prev.filter((f) => f.id !== "type"), { id: "type", value }]
: prev.filter((f) => f.id !== "type")
)
setPagination((prev) => ({ ...prev, pageIndex: 0 }))
}
// In a real app, use useQuery with pagination/sorting/filters as query keys
const { data, totalItems } = useServerData({
pagination,
sorting,
globalFilter,
statusFilter,
typeFilter,
})
return (
<DataTable
columns={columns}
data={data}
enableSorting
manualSorting
enableRowSelection
enableGlobalFilter
enableFiltering
manualFiltering
sorting={sorting}
onSortingChange={setSorting}
globalFilter={globalFilter}
onGlobalFilterChange={setGlobalFilter}
columnFilters={columnFilters}
onColumnFiltersChange={setColumnFilters}
rowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
pagination={{
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
totalItems: totalItems,
onPaginationChange: setPagination,
}}
>
<DataTableToolbar>
<input
type="text"
placeholder="Search..."
value={globalFilter}
onChange={(e) => {
setGlobalFilter(e.target.value)
setPagination((prev) => ({ ...prev, pageIndex: 0 }))
}}
/>
<select
value={statusFilter ?? ""}
onChange={(e) => setStatusFilter(e.target.value || undefined)}
>
<option value="">All statuses</option>
<option value="paid">Paid</option>
<option value="pending">Pending</option>
</select>
<select
value={typeFilter ?? ""}
onChange={(e) => setTypeFilter(e.target.value || undefined)}
>
<option value="">All types</option>
<option value="credit">Credit</option>
<option value="debit">Debit</option>
</select>
{columnFilters.length > 0 && (
<Button
variant="tertiary"
size="sm"
onClick={() => {
setColumnFilters([])
setPagination((prev) => ({ ...prev, pageIndex: 0 }))
}}
>
Clear filters
</Button>
)}
<div className="flex-1" />
<DataTableColumnVisibility label="Toggle columns" />
</DataTableToolbar>
<DataTableContent>
<DataTableHeader />
<DataTableBody />
</DataTableContent>
<DataTablePagination />
</DataTable>
)
}Cell Overflow
Control how cell content handles overflow with the cellOverflow prop. This is useful when cells contain long text that would otherwise expand the column width.
grow(default) - Cells expand to fit contentwrap- Text wraps within the column widthtruncate- Single line with ellipsis for overflow
| Invoice | Customer | Description | Amount | |
|---|---|---|---|---|
| INV-0001 | John Doe | john.doe@example.com | This is a very long description that demonstrates how text truncation works when the content exceeds the column width. It should show an ellipsis. | €150.00 |
| INV-0002 | Jane Smith | jane.smith.at.a.very.long.company.name@longdomainexample.com | Another long description that will be truncated to fit within the available space in the cell. | €250.00 |
| INV-0003 | Bob Wilson with a very long name that might need to wrap or truncate | bob@example.com | Short desc | €350.00 |
<DataTable columns={columns} data={data} cellOverflow="truncate">
<DataTableContent />
</DataTable>Per-Column Override
Override the table default for specific columns using meta.cellOverflow:
const columns: ColumnDef<Data>[] = [
{
accessorKey: "description",
header: "Description",
size: 200,
meta: { cellOverflow: "wrap" }, // This column wraps, others truncate
},
{
accessorKey: "email",
header: "Email",
// Uses table default
},
]
<DataTable columns={columns} data={data} cellOverflow="truncate">
<DataTableContent />
</DataTable>For more advanced truncation like multi-line (line-clamp-2), use a custom cell renderer with your own CSS classes.
With Sorting
Enable sorting by setting enableSorting and using DataTableColumnHeader for sortable columns.
| Invoice | Status | Type | Customer | Amount | |
|---|---|---|---|---|---|
| INV-0001 | Paid | credit | John Doe | john.doe@example.com | €321.00 |
| INV-0002 | Pending | debit | Jane Smith | jane.smith@example.com | €106.00 |
| INV-0003 | Pending | debit | Bob Wilson | bob.wilson@example.com | €348.00 |
| INV-0004 | Overdue | credit | Alice Brown | alice.brown@example.com | €421.00 |
| INV-0005 | Overdue | credit | Charlie Davis | charlie.davis@example.com | €399.00 |
| INV-0006 | Pending | credit | Eva Martinez | eva.martinez@example.com | €529.00 |
| INV-0007 | Pending | debit | Frank Miller | frank.miller@example.com | €546.00 |
| INV-0008 | Pending | credit | Grace Lee | grace.lee@example.com | €299.00 |
| INV-0009 | Paid | credit | Henry Chen | henry.chen@example.com | €387.00 |
| INV-0010 | Paid | credit | Isabel Garcia | isabel.garcia@example.com | €476.00 |
import { DataTable, DataTableContent, DataTableColumnHeader } from "@epilot/volt-ui"
import type { ColumnDef } from "@tanstack/react-table"
const columns: ColumnDef<Invoice>[] = [
{
accessorKey: "id",
header: ({ column }) => <DataTableColumnHeader column={column} title="Invoice" />,
},
{
accessorKey: "customer",
header: ({ column }) => <DataTableColumnHeader column={column} title="Customer" />,
},
// ...more columns
]
<DataTable columns={columns} data={data} enableSorting>
<DataTableContent />
</DataTable>With Footer
Use DataTableFooter to add custom content between the table and pagination, such as "Show more" links or summary information.
| Invoice | Status | Type | Customer | Amount | |
|---|---|---|---|---|---|
| INV-0001 | Paid | credit | John Doe | john.doe@example.com | €321.00 |
| INV-0002 | Pending | debit | Jane Smith | jane.smith@example.com | €106.00 |
| INV-0003 | Pending | debit | Bob Wilson | bob.wilson@example.com | €348.00 |
| INV-0004 | Overdue | credit | Alice Brown | alice.brown@example.com | €421.00 |
| INV-0005 | Overdue | credit | Charlie Davis | charlie.davis@example.com | €399.00 |
import { useState } from "react"
import { DataTable, DataTableContent, DataTableFooter, Button } from "@epilot/volt-ui"
function MyTable() {
const [pageSize, setPageSize] = useState(5)
const visibleData = data.slice(0, pageSize)
const hasMore = pageSize < data.length
return (
<DataTable columns={columns} data={visibleData}>
<DataTableContent />
{hasMore && (
<DataTableFooter className="justify-start">
<Button size="sm" variant="tertiary" onClick={() => setPageSize((prev) => prev + 3)}>
Show more ({data.length - pageSize} remaining)
</Button>
</DataTableFooter>
)}
</DataTable>
)
}Column Definitions
DataTable uses Tanstack Table's column definition format. Here are common patterns:
Basic Column
{
accessorKey: "name",
header: "Name",
}Custom Cell Rendering
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => <Badge>{row.getValue("status")}</Badge>,
}Sortable Column
{
accessorKey: "date",
header: ({ column }) => <DataTableColumnHeader column={column} title="Date" />,
}Right-Aligned Column
{
accessorKey: "amount",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => (
<div className="text-right tabular-nums">
{formatCurrency(row.getValue("amount"))}
</div>
),
meta: { align: "right" },
}Right-Aligned Sortable Column
{
accessorKey: "amount",
header: ({ column }) => (
<div className="flex justify-end">
<DataTableColumnHeader column={column} title="Amount" />
</div>
),
cell: ({ row }) => (
<div className="text-right tabular-nums">
{formatCurrency(row.getValue("amount"))}
</div>
),
meta: { align: "right" }, // Ensures proper header padding alignment
}API Reference
See the full API Reference for all props and configuration options.