Volt UI
ComponentsData Table

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.

InvoiceStatusTypeCustomerEmail
Amount
INV-0001PaidcreditJohn Doejohn.doe@example.com
€321.00
INV-0002PendingdebitJane Smithjane.smith@example.com
€106.00
INV-0003PendingdebitBob Wilsonbob.wilson@example.com
€348.00
INV-0004OverduecreditAlice Brownalice.brown@example.com
€421.00
INV-0005OverduecreditCharlie Davischarlie.davis@example.com
€399.00
INV-0006PendingcreditEva Martinezeva.martinez@example.com
€529.00
INV-0007PendingdebitFrank Millerfrank.miller@example.com
€546.00
INV-0008PendingcreditGrace Leegrace.lee@example.com
€299.00
INV-0009PaidcreditHenry Chenhenry.chen@example.com
€387.00
INV-0010PaidcreditIsabel Garciaisabel.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.

InvoiceStatusTypeCustomerEmail
Amount
INV-0001PaidcreditJohn Doejohn.doe@example.com
€321.00
INV-0002PendingdebitJane Smithjane.smith@example.com
€106.00
INV-0003PendingdebitBob Wilsonbob.wilson@example.com
€348.00
INV-0004OverduecreditAlice Brownalice.brown@example.com
€421.00
INV-0005OverduecreditCharlie Davischarlie.davis@example.com
€399.00
INV-0006PendingcreditEva Martinezeva.martinez@example.com
€529.00
INV-0007PendingdebitFrank Millerfrank.miller@example.com
€546.00
INV-0008PendingcreditGrace Leegrace.lee@example.com
€299.00
INV-0009PaidcreditHenry Chenhenry.chen@example.com
€387.00
INV-0010PaidcreditIsabel Garciaisabel.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.

InvoiceStatusTypeCustomerEmail
Amount
INV-0001PaidcreditJohn Doejohn.doe@example.com
€321.00
INV-0002PendingdebitJane Smithjane.smith@example.com
€106.00
INV-0003PendingdebitBob Wilsonbob.wilson@example.com
€348.00
INV-0004OverduecreditAlice Brownalice.brown@example.com
€421.00
INV-0005OverduecreditCharlie Davischarlie.davis@example.com
€399.00
INV-0006PendingcreditEva Martinezeva.martinez@example.com
€529.00
INV-0007PendingdebitFrank Millerfrank.miller@example.com
€546.00
INV-0008PendingcreditGrace Leegrace.lee@example.com
€299.00
INV-0009PaidcreditHenry Chenhenry.chen@example.com
€387.00
INV-0010PaidcreditIsabel Garciaisabel.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 headercolumn.id

Column Resizing

Column resizing is enabled by default. Users can resize columns by dragging the header borders. Use disableColumnResizing to opt out.

Column resizing is enabled by default. Drag the edges between column headers to resize. The "Type" column has resizing disabled via column config.
InvoiceStatusTypeCustomerEmail
Amount
INV-0001PaidcreditJohn Doejohn.doe@example.com
€321.00
INV-0002PendingdebitJane Smithjane.smith@example.com
€106.00
INV-0003PendingdebitBob Wilsonbob.wilson@example.com
€348.00
INV-0004OverduecreditAlice Brownalice.brown@example.com
€421.00
INV-0005OverduecreditCharlie Davischarlie.davis@example.com
€399.00
INV-0006PendingcreditEva Martinezeva.martinez@example.com
€529.00
INV-0007PendingdebitFrank Millerfrank.miller@example.com
€546.00
INV-0008PendingcreditGrace Leegrace.lee@example.com
€299.00
INV-0009PaidcreditHenry Chenhenry.chen@example.com
€387.00
INV-0010PaidcreditIsabel Garciaisabel.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:

Current widths: {"id":100,"customer":180,"email":220}
InvoiceStatusTypeCustomerEmail
Amount
INV-0001PaidcreditJohn Doejohn.doe@example.com
€321.00
INV-0002PendingdebitJane Smithjane.smith@example.com
€106.00
INV-0003PendingdebitBob Wilsonbob.wilson@example.com
€348.00
INV-0004OverduecreditAlice Brownalice.brown@example.com
€421.00
INV-0005OverduecreditCharlie Davischarlie.davis@example.com
€399.00
INV-0006PendingcreditEva Martinezeva.martinez@example.com
€529.00
INV-0007PendingdebitFrank Millerfrank.miller@example.com
€546.00
INV-0008PendingcreditGrace Leegrace.lee@example.com
€299.00
INV-0009PaidcreditHenry Chenhenry.chen@example.com
€387.00
INV-0010PaidcreditIsabel Garciaisabel.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>
ValueBehavior
"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.

Select a row to pin the checkbox column to the left edge.
InvoiceStatusTypeCustomerEmail
Amount
INV-0001PaidcreditJohn Doejohn.doe@example.com
€321.00
INV-0002PendingdebitJane Smithjane.smith@example.com
€106.00
INV-0003PendingdebitBob Wilsonbob.wilson@example.com
€348.00
INV-0004OverduecreditAlice Brownalice.brown@example.com
€421.00
INV-0005OverduecreditCharlie Davischarlie.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.

The "Invoice" column is pinned to the left. Scroll horizontally to see the effect.
InvoiceStatusTypeCustomerEmail
Amount
INV-0001PaidcreditJohn Doejohn.doe@example.com
€321.00
INV-0002PendingdebitJane Smithjane.smith@example.com
€106.00
INV-0003PendingdebitBob Wilsonbob.wilson@example.com
€348.00
INV-0004OverduecreditAlice Brownalice.brown@example.com
€421.00
INV-0005OverduecreditCharlie Davischarlie.davis@example.com
€399.00
INV-0006PendingcreditEva Martinezeva.martinez@example.com
€529.00
INV-0007PendingdebitFrank Millerfrank.miller@example.com
€546.00
INV-0008PendingcreditGrace Leegrace.lee@example.com
€299.00
INV-0009PaidcreditHenry Chenhenry.chen@example.com
€387.00
INV-0010PaidcreditIsabel Garciaisabel.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.

density="compact"
InvoiceStatusTypeCustomerEmail
Amount
INV-0001PaidcreditJohn Doejohn.doe@example.com
€321.00
INV-0002PendingdebitJane Smithjane.smith@example.com
€106.00
INV-0003PendingdebitBob Wilsonbob.wilson@example.com
€348.00
density="normal" (default)
InvoiceStatusTypeCustomerEmail
Amount
INV-0001PaidcreditJohn Doejohn.doe@example.com
€321.00
INV-0002PendingdebitJane Smithjane.smith@example.com
€106.00
INV-0003PendingdebitBob Wilsonbob.wilson@example.com
€348.00
density="comfortable"
InvoiceStatusTypeCustomerEmail
Amount
INV-0001PaidcreditJohn Doejohn.doe@example.com
€321.00
INV-0002PendingdebitJane Smithjane.smith@example.com
€106.00
INV-0003PendingdebitBob Wilsonbob.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>
ValueHeader HeightCell Padding
"compact"h-8 (32px)py-1.5 (6px)
"normal"h-9 (36px)py-2 (8px)
"comfortable"h-10 (40px)py-3 (12px)