Volt UI
ComponentsData Table

Advanced

Virtualization, loading/error states, and composable API.

Virtualization

For large datasets (1,000+ rows), enable virtualization to render only visible rows. This dramatically improves performance by only rendering rows that are currently in the viewport.

Displaying 1,000 rows with virtualization
InvoiceStatusTypeCustomerEmail
Amount
import { useState, useMemo } from "react"
import { DataTable, DataTableContent } from "@epilot/volt-ui"

function VirtualizedTable() {
  const [rowSelection, setRowSelection] = useState({})
  const largeDataset = useMemo(() => generateLargeData(15000), [])

  return (
    <DataTable
      columns={columns}
      data={largeDataset}
      enableSorting
      enableRowSelection
      rowSelection={rowSelection}
      onRowSelectionChange={setRowSelection}
      virtualization={{
        enabled: true,
        estimateRowHeight: 48, // Match your row height
        overscan: 10,          // Render 10 extra rows beyond viewport
        containerHeight: 600,  // Fixed height required for virtualization
      }}
    >
      <DataTableContent />
    </DataTable>
  )
}

Virtualization and pagination are mutually exclusive. When virtualization is enabled, pagination is automatically disabled.

With virtualization enabled, features like sorting and row selection still work normally. The virtualization is transparent to the user - they can scroll through all rows seamlessly while only ~20-30 rows are actually rendered in the DOM at any time.

Virtualization Configuration

type VirtualizationConfig = {
  enabled: boolean
  estimateRowHeight?: number // Default: 48
  overscan?: number          // Default: 10
  containerHeight?: number   // Default: 600
}

Composable API

For more control, pass children to DataTableContent to explicitly render DataTableHeader and DataTableBody. This lets you customize props like sticky headers or custom empty states.

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,
  DataTableHeader,
  DataTableBody,
  DataTablePagination,
  DataTableToolbar,
} from "@epilot/volt-ui"

function ComposableTable() {
  return (
    <DataTable
      columns={columns}
      data={data}
      enableSorting
      pagination={{ pageSize: 10 }}
    >
      <DataTableToolbar>
        <input placeholder="Search invoices..." />
      </DataTableToolbar>

      <DataTableContent>
        <DataTableHeader />
        <DataTableBody />
      </DataTableContent>

      <DataTablePagination className="mt-4" />
    </DataTable>
  )
}

With Virtualization

When using virtualization, you can use DataTableHeader with sticky prop and customize the empty state.

Composable API with 1,000 rows
InvoiceStatusTypeCustomerEmail
Amount
<DataTable
  columns={columns}
  data={largeDataset}
  enableSorting
  virtualization={{ enabled: true, containerHeight: 500 }}
>
  <DataTableContent>
    <DataTableHeader sticky />
    <DataTableBody emptyState="No results found." />
  </DataTableContent>
</DataTable>

When to Use Explicit Header/Body

Use the shorthand <DataTableContent /> for most cases. Pass children when you need:

  • Sticky headers: <DataTableHeader sticky /> for fixed headers during scroll
  • Custom empty state: <DataTableBody emptyState="..." /> for custom messages
  • Loading/error states: <DataTableBody isLoading={...} loadingState={...} /> for async data
  • Custom wrappers: Add divs or other elements between header and body
  • Conditional rendering: Show/hide header or body based on state

Loading State

Show a loading indicator while fetching data. Use DataTableLoading with DataTableBody props.

InvoiceStatusTypeCustomerEmail
Amount
import { DataTable, DataTableContent, DataTableHeader, DataTableBody, DataTableLoading } from "@epilot/volt-ui"

function MyTable() {
  const { data, isLoading } = useQuery(...)

  return (
    <DataTable columns={columns} data={data ?? []}>
      <DataTableContent>
        <DataTableHeader />
        <DataTableBody
          isLoading={isLoading}
          loadingState={<DataTableLoading />}
          emptyState="No invoices found."
        />
      </DataTableContent>
    </DataTable>
  )
}

Custom Loading Content

For i18n support or custom styling, pass children to DataTableLoading:

InvoiceStatusTypeCustomerEmail
Amount

Fetching invoices...

This may take a moment

<DataTableBody
  isLoading={isLoading}
  loadingState={
    <DataTableLoading>
      <Spinner size="xl" className="text-accent-light" />
      <p className="mt-4 text-base font-medium">{t('table.loading')}</p>
      <p className="text-sm text-gray-light">{t('table.loading.description')}</p>
    </DataTableLoading>
  }
/>

Error State

Show an error message with optional retry button when data fetching fails.

InvoiceStatusTypeCustomerEmail
Amount
import { DataTable, DataTableContent, DataTableHeader, DataTableBody, DataTableError } from "@epilot/volt-ui"

function MyTable() {
  const { data, isLoading, isError, refetch } = useQuery(...)

  return (
    <DataTable columns={columns} data={data ?? []}>
      <DataTableContent>
        <DataTableHeader />
        <DataTableBody
          isLoading={isLoading}
          loadingState={<DataTableLoading />}
          isError={isError}
          errorState={
            <DataTableError
              title="Failed to load invoices"
              description="There was a problem connecting to the server."
              retryLabel="Retry"
              onRetry={refetch}
            />
          }
          emptyState="No invoices found."
        />
      </DataTableContent>
    </DataTable>
  )
}

Custom Error Content

For i18n support or fully custom error UI:

InvoiceStatusTypeCustomerEmail
Amount
<DataTableBody
  isError={isError}
  errorState={
    <DataTableError>
      <AlertTriangle size={48} className="text-error-light" />
      <p className="mt-2 text-lg font-semibold">{t('table.error.title')}</p>
      <p className="text-sm text-gray-light">{t('table.error.description')}</p>
      <div className="flex gap-2 mt-4">
        <Button variant="secondary" size="sm" onClick={refetch}>
          {t('retry')}
        </Button>
        <Button variant="tertiary" size="sm">
          {t('contact_support')}
        </Button>
      </div>
    </DataTableError>
  }
/>

State Priority: When multiple states are active, the priority is: error > loading > empty > data. This ensures errors are always visible even during loading transitions.

Row Styling

Row Borders

Control visibility of horizontal border lines on header and data rows using showHeaderBorder and showRowBorders props. Both default to true.

// Default - borders visible on header and rows
<DataTable columns={columns} data={data} />

// Hide all borders
<DataTable
  columns={columns}
  data={data}
  showHeaderBorder={false}
  showRowBorders={false}
/>

// Hide only row borders (keep header border)
<DataTable
  columns={columns}
  data={data}
  showRowBorders={false}
/>

// Hide only header border (keep row borders)
<DataTable
  columns={columns}
  data={data}
  showHeaderBorder={false}
/>

Row borders use box-shadow instead of CSS border to work correctly with the table's border-separate layout (required for column resizing).

Sticky Header & Pinned Column Background

When using sticky headers or pinned columns, a background color is applied to prevent content from showing through during scroll. Customize this using the stickyBackground prop on DataTableHeader.

// Default uses var(--volt-gray-2)
<DataTableHeader sticky />

// Custom background color
<DataTableHeader sticky stickyBackground="white" />

// Use a different volt gray
<DataTableHeader sticky stickyBackground="var(--volt-gray-1)" />

// Custom CSS variable
<DataTableHeader sticky stickyBackground="var(--my-header-bg)" />

The stickyBackground prop applies to both sticky headers AND pinned header cells. Pinned body cells inherit their background from the table row.

CSS Variables Reference:

VariableUsage
--volt-gray-2Default background for sticky headers and pinned header cells
--volt-gray-a6Border color for header and row separators