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.
| Invoice | Status | Type | Customer | 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.
| 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,
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.
| Invoice | Status | Type | Customer | 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.
| Invoice | Status | Type | Customer | 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:
| Invoice | Status | Type | Customer | 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.
| Invoice | Status | Type | Customer | Amount | |
|---|---|---|---|---|---|
Failed to load invoices There was a problem connecting to the server. Please check your connection and try again. | |||||
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:
| Invoice | Status | Type | Customer | Amount | |
|---|---|---|---|---|---|
Oops! Something went wrong We couldn't load your invoices. Please try again or contact support if the problem persists. | |||||
<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:
| Variable | Usage |
|---|---|
--volt-gray-2 | Default background for sticky headers and pinned header cells |
--volt-gray-a6 | Border color for header and row separators |