Columns & Filtering
Column visibility, resizing, pinning, and filtering options.
Global Filtering
Global filtering searches across all columns simultaneously with a single search input. Enable it with enableGlobalFilter and manage the filter value with globalFilter and onGlobalFilterChange.
| 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 { useState } from "react"
import { DataTable, DataTableContent, DataTableToolbar } from "@epilot/volt-ui"
import { IconSearch } from "@tabler/icons-react"
function MyTable() {
const [globalFilter, setGlobalFilter] = useState("")
return (
<DataTable
columns={columns}
data={data}
enableGlobalFilter
globalFilter={globalFilter}
onGlobalFilterChange={setGlobalFilter}
>
<DataTableToolbar>
<div className="flex items-center gap-2 rounded-lg border border-gray-a6 px-3 py-1.5">
<IconSearch size={18} />
<input
type="text"
placeholder="Search all columns..."
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
/>
</div>
</DataTableToolbar>
<DataTableContent />
</DataTable>
)
}Column Filtering
Column filtering allows filtering individual columns independently. Each column can have its own filter control (dropdowns, text inputs, date pickers, etc.). Enable it with enableFiltering and manage filters with columnFilters and onColumnFiltersChange.
| 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 { useState } from "react"
import type { ColumnFiltersState } from "@tanstack/react-table"
import { DataTable, DataTableContent, DataTableToolbar, Button } from "@epilot/volt-ui"
function MyTable() {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
// Get current filter values
const statusFilter = columnFilters.find((f) => f.id === "status")?.value as string | undefined
const typeFilter = columnFilters.find((f) => f.id === "type")?.value as string | undefined
// Update status filter
const setStatusFilter = (value: string | undefined) => {
setColumnFilters((prev) =>
value
? [...prev.filter((f) => f.id !== "status"), { id: "status", value }]
: prev.filter((f) => f.id !== "status")
)
}
// Update type filter
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}
enableFiltering
columnFilters={columnFilters}
onColumnFiltersChange={setColumnFilters}
>
<DataTableToolbar>
<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>
<option value="overdue">Overdue</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>
)}
</DataTableToolbar>
<DataTableContent />
</DataTable>
)
}Global vs Column Filtering: Use global filtering for a simple search-all experience. Use column filtering when users need to filter specific fields (e.g., status dropdowns, date ranges). You can combine both for advanced filtering UIs.
Column Visibility
Allow users to show/hide columns with the DataTableColumnVisibility dropdown. Place it in the toolbar for easy access.
| 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 { useState } from "react"
import {
DataTable,
DataTableContent,
DataTableToolbar,
DataTableColumnVisibility,
} from "@epilot/volt-ui"
function MyTable() {
return (
<DataTable columns={columns} data={data} enableSorting>
<DataTableToolbar>
<div className="flex-1" />
{/* Use -mr-1 wrapper to align icon with toolbar edge (compensates for button hover padding) */}
<div className="-mr-1">
<DataTableColumnVisibility label="Toggle columns" />
</div>
</DataTableToolbar>
<DataTableContent />
</DataTable>
)
}Preventing Column Hiding
To prevent specific columns from being hidden (like ID or actions columns), set enableHiding: false in the column definition:
const columns: ColumnDef<Invoice>[] = [
{
accessorKey: "id",
header: "Invoice",
enableHiding: false, // This column won't appear in the visibility dropdown
},
{
accessorKey: "status",
header: "Status",
// enableHiding defaults to true
},
{
id: "actions",
cell: ({ row }) => <ActionsMenu row={row} />,
enableHiding: false, // Actions column should always be visible
},
]Custom Trigger
Replace the default trigger button with your own:
import { Button } from "@epilot/volt-ui"
<DataTableColumnVisibility
label="Toggle columns"
trigger={
<Button variant="tertiary" size="sm">
Columns
</Button>
}
/>Custom Visibility Label
For columns without a string header (like actions columns), use meta.visibilityLabel to specify a custom label for the visibility dropdown:
const columns: ColumnDef<Invoice>[] = [
// ... other columns
{
id: "actions",
cell: ({ row }) => <ActionsMenu row={row} />,
meta: {
visibilityLabel: "Actions", // Label shown in visibility dropdown
},
},
]The label priority is: meta.visibilityLabel → string header → column.id
Column Resizing
Column resizing is enabled by default. Users can resize columns by dragging the header borders. Use disableColumnResizing to opt out.
| 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"
const columns: ColumnDef<Invoice>[] = [
{
accessorKey: "id",
header: "Invoice",
size: 120, // Initial width
minSize: 80, // Minimum width when resizing
maxSize: 200, // Maximum width when resizing
},
{
accessorKey: "status",
header: "Status",
size: 120,
},
{
accessorKey: "type",
header: "Type",
size: 100,
enableResizing: false, // Disable resizing for this column
},
// ...more columns
]
// Column resizing is enabled by default
<DataTable columns={columns} data={data}>
<DataTableContent />
</DataTable>
// To disable column resizing:
<DataTable columns={columns} data={data} disableColumnResizing>
<DataTableContent />
</DataTable>Controlled Mode
For full control over column widths (e.g., to persist to localStorage), use controlled state:
| 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 { useState } from "react"
import type { ColumnSizingState } from "@tanstack/react-table"
import { DataTable, DataTableContent } from "@epilot/volt-ui"
function MyTable() {
const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({
id: 100,
customer: 180,
})
return (
<DataTable
columns={columns}
data={data}
columnSizing={columnSizing}
onColumnSizingChange={setColumnSizing}
>
<DataTableContent />
</DataTable>
)
}Per-Column Configuration
Control resizing behavior per column in the column definition:
const columns: ColumnDef<Data>[] = [
{
accessorKey: "id",
header: "ID",
size: 60,
enableResizing: false, // Cannot be resized
},
{
accessorKey: "name",
header: "Name",
size: 150,
minSize: 100, // Minimum 100px
maxSize: 400, // Maximum 400px
},
{
accessorKey: "email",
header: "Email",
// Uses defaults: minSize=20, maxSize=unlimited
},
]The selection checkbox column automatically has enableResizing: false set, so it cannot be resized.
Resize Handle Visibility
By default, resize handles only appear when hovering over the header row for a cleaner look. Use resizeHandleVisibility to control this behavior:
// Default: handles appear on header hover
<DataTable columns={columns} data={data}>
<DataTableContent />
</DataTable>
// Always show resize handles
<DataTable columns={columns} data={data} resizeHandleVisibility="always">
<DataTableContent />
</DataTable>| Value | Behavior |
|---|---|
"onHeaderHover" | Handles appear when hovering the header row (default) |
"always" | Handles are always visible |
Column Pinning
Pin columns to the left or right edge of the table so they remain visible during horizontal scrolling. This is useful for keeping important columns like checkboxes or IDs always visible.
Auto-Pin Selection Column
When using row selection, enable autoPinSelection to automatically pin the checkbox column when rows are selected. The column unpins when all rows are deselected.
| 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 type { RowSelectionState } from "@tanstack/react-table"
import { DataTable, DataTableContent } from "@epilot/volt-ui"
function MyTable() {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
return (
<DataTable
columns={columns}
data={data}
enableRowSelection
rowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
autoPinSelection // Pin checkbox column when rows are selected
>
<DataTableContent />
</DataTable>
)
}Manual Column Pinning
Pin specific columns using the columnPinning prop. Columns can be pinned to the left or right edge.
| 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"
<DataTable
columns={columns}
data={data}
columnPinning={{ left: ["id"], right: ["actions"] }}
>
<DataTableContent />
</DataTable>Controlled Pinning State
For dynamic pinning (e.g., letting users pin/unpin columns), use controlled state:
import { useState } from "react"
import type { ColumnPinningState } from "@epilot/volt-ui"
import { DataTable, DataTableContent } from "@epilot/volt-ui"
function MyTable() {
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({
left: ["id"],
right: [],
})
return (
<DataTable
columns={columns}
data={data}
columnPinning={columnPinning}
onColumnPinningChange={setColumnPinning}
>
<DataTableContent />
</DataTable>
)
}To customize the background color of pinned columns and sticky headers, see Sticky Header & Pinned Column Background in the Advanced section.
Density
Control row height with the density prop. Choose between compact, normal (default), or comfortable for different spacing levels.
| 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 |
| 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 |
| 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 |
import { DataTable, DataTableContent } from "@epilot/volt-ui"
// Compact rows (smallest)
<DataTable columns={columns} data={data} density="compact">
<DataTableContent />
</DataTable>
// Normal rows (default)
<DataTable columns={columns} data={data} density="normal">
<DataTableContent />
</DataTable>
// Comfortable rows (largest)
<DataTable columns={columns} data={data} density="comfortable">
<DataTableContent />
</DataTable>| Value | Header Height | Cell Padding |
|---|---|---|
"compact" | h-8 (32px) | py-1.5 (6px) |
"normal" | h-9 (36px) | py-2 (8px) |
"comfortable" | h-10 (40px) | py-3 (12px) |