Skip to content

VirtualTable

VirtualTable is a compound component that renders a virtualized, scrollable table. Only the rows within (and slightly outside) the viewport are mounted, keeping DOM size constant regardless of dataset size. All rows — header, body, skeleton, empty — share the same column layout.

import { VirtualTable } from '@requence/table'
<VirtualTable totalCount={1000} rowHeight={40}>
<VirtualTable.Header>
<VirtualTable.Column width="2fr">Name</VirtualTable.Column>
<VirtualTable.Column width="1fr">Email</VirtualTable.Column>
</VirtualTable.Header>
<VirtualTable.Body>
{(index) => {
const item = getItem(index)
if (!item) return null
return (
<VirtualTable.Row>
<VirtualTable.Cell>{item.name}</VirtualTable.Cell>
<VirtualTable.Cell>{item.email}</VirtualTable.Cell>
</VirtualTable.Row>
)
}}
</VirtualTable.Body>
<VirtualTable.SkeletonRow>
<VirtualTable.Cell>Loading…</VirtualTable.Cell>
</VirtualTable.SkeletonRow>
<VirtualTable.Empty>No results found.</VirtualTable.Empty>
<VirtualTable.Footer>
{({ start, end }) => <span>Showing {start}{end}</span>}
</VirtualTable.Footer>
</VirtualTable>
PropTypeDefaultDescription
totalCountnumberRequired. Total number of rows in the dataset.
rowHeightnumberRequired. Fixed height of each row in pixels.
overscannumber5Number of extra rows rendered above and below the viewport.
onRangeChange(range: { start: number; end: number }) => voidCalled when the visible row range changes. Wire this to useTableCache().handleRangeChange to trigger page fetches.
classNamestringAdditional class name for the outer container.
styleCSSPropertiesAdditional inline styles for the outer container.
aria-labelstringAccessible label for the table element.
childrenReactNodeSub-components (Header, Body, SkeletonRow, Empty, Footer).

Wraps the column definitions. Renders as a sticky header row.

PropTypeDefaultDescription
classNamestringAdditional class name for the header row group.
childrenReactNodeOne or more VirtualTable.Column elements.

Defines a single column’s width and header content.

PropTypeDefaultDescription
widthnumber | string'1fr'Column width. A number is treated as pixels. A string is used as a flexible unit (e.g. '1fr', '2fr').
classNamestringAdditional class name for the header cell.
resizablebooleanfalseWhether the column can be resized by dragging a handle on its right edge.
minWidthnumber50Minimum width in pixels during resize.
maxWidthnumberMaximum width in pixels during resize.
transparentbooleanMarks the column as transparent — the row background will not extend behind it. Useful for action columns that sit outside the visual row.
onResizeStart() => voidCalled when a resize drag starts.
onResizeEnd(width: number, startWidth: number, frValue: number) => voidCalled when a resize drag ends. See Column Resizing.
childrenReactNodeHeader cell content.
  • Number (e.g. 200) — Fixed pixel width. The column will always be exactly that many pixels wide and does not participate in flexible distribution.
  • String (e.g. '1fr', '2fr') — Fractional unit. Columns share remaining space proportionally. The default is '1fr'.
  • When minWidth is set, the column will not collapse below that size.

When resizable is true, a drag handle appears on the column’s right edge. During the drag:

  1. The column width is updated in real time.
  2. The width is clamped between minWidth and maxWidth (and the available container width).
  3. onResizeStart() is called when dragging begins.
  4. onResizeEnd(width, startWidth, frValue) is called when the mouse is released:
    • width — The final pixel width of the column.
    • startWidth — The pixel width before the drag started.
    • frValue — The equivalent fr value relative to the other flexible columns. Use this when you want to persist proportional widths rather than fixed pixels.

Provides the render function for each row.

PropTypeDefaultDescription
children(index: number) => ReactNode | nullRequired. Render function called for each visible row index. Return null to render the skeleton row instead (e.g. when the item hasn’t been fetched yet).

Wrapper element for a data row. Extends all standard div props.

PropTypeDefaultDescription
classNamestringAdditional class name merged onto the row.
styleCSSPropertiesAdditional inline styles.
...restComponentProps<'div'>All other div props are forwarded (e.g. onClick, onContextMenu).

A single cell within a row. Extends all standard div props.

PropTypeDefaultDescription
showOnHoverbooleanWhen true, cell content is hidden by default and only appears when the row is hovered. Useful for action buttons.
colSpannumberNumber of columns this cell spans. Sets grid-column: span N.
classNamestringAdditional class name for the cell.
...restComponentProps<'div'>All other div props are forwarded.

Placeholder row rendered when Body.children returns null for an index. Extends all standard div props.

PropTypeDefaultDescription
classNamestringAdditional class name for the skeleton row.
childrenReactNodeSkeleton content (e.g. shimmer elements).
...restComponentProps<'div'>All other div props are forwarded.

Shown when totalCount is 0. Rendered inside a centered flex container below the header.

PropTypeDefaultDescription
classNamestringAdditional class name for the empty state container.
childrenReactNodeEmpty state content.

Rendered below the scrollable area when totalCount > 0. Receives the current visible range.

PropTypeDefaultDescription
classNamestringAdditional class name for the footer container.
children(range: { start: number; end: number }) => ReactNodeRequired. Render function that receives the visible range.

Factory functions create pre-configured sub-components with default props baked in. This is useful when you want a consistent look across multiple tables without repeating the same props.

import {
createTableHeader,
createTableColumn,
createTableBody,
createTableRow,
createTableSkeletonRow,
createTableEmpty,
createTableFooter,
} from '@requence/table'

Each factory takes an optional Partial of the corresponding props type and returns a component that can be used in place of the built-in sub-component:

const Header = createTableHeader({ className: 'bg-gray-100 border-b' })
const Column = createTableColumn({ resizable: true, minWidth: 80 })
const Body = createTableBody()
const Row = createTableRow({ className: 'hover:bg-gray-50 border-b' })
const SkeletonRow = createTableSkeletonRow({ className: 'animate-pulse' })
const Empty = createTableEmpty({ className: 'py-20 text-gray-400' })
const Footer = createTableFooter({ className: 'px-4 py-2 text-sm' })

Then use them as drop-in replacements:

<VirtualTable totalCount={totalCount} rowHeight={40}>
<Header>
<Column width="2fr">Name</Column>
<Column width="1fr">Email</Column>
</Header>
<Body>
{(index) => {
const item = getItem(index)
if (!item) return null
return (
<Row>
<VirtualTable.Cell>{item.name}</VirtualTable.Cell>
<VirtualTable.Cell>{item.email}</VirtualTable.Cell>
</Row>
)
}}
</Body>
<SkeletonRow>
<VirtualTable.Cell colSpan={2}>Loading…</VirtualTable.Cell>
</SkeletonRow>
<Empty>No results found.</Empty>
<Footer>
{({ start, end }) => <span>Showing {start}{end}</span>}
</Footer>
</VirtualTable>

Props passed at the usage site are merged with the factory defaults. className values are merged via tailwind-merge, so utility conflicts are resolved correctly.