Virtual Table
Compound component pattern
Section titled “Compound component pattern”VirtualTable is a compound component. Instead of passing a monolithic config object, you compose the table from sub-components:
<VirtualTable totalCount={100} rowHeight={40}> <VirtualTable.Header> <VirtualTable.Column>Name</VirtualTable.Column> <VirtualTable.Column>Email</VirtualTable.Column> </VirtualTable.Header>
<VirtualTable.Body> {(index) => { /* ... */ }} </VirtualTable.Body></VirtualTable>Each sub-component — Header, Column, Body, Row, Cell, SkeletonRow, Empty, Footer — is attached to the VirtualTable namespace.
How sub-components work
Section titled “How sub-components work”The sub-components are declarative — you use them to describe the table’s structure, and VirtualTable takes care of rendering the actual DOM.
This design means:
- No wrapper divs from sub-components polluting the DOM tree
- Full control over the rendered output stays with
VirtualTable - ClassNames merge via
tailwind-merge— defaults from factory functions are combined with per-instance overrides
Column layout
Section titled “Column layout”Column widths are defined on each VirtualTable.Column. All rows — header, body, and skeleton — share the same column widths, keeping everything perfectly aligned.
| Width value | Use case |
|---|---|
'1fr' | Proportional, flexible |
'2fr' | Double-width proportional |
200 | Fixed pixel width |
| (omitted) | Default — equal distribution |
When minWidth is set on a column, it prevents the column from collapsing below that size.
Virtual scrolling
Section titled “Virtual scrolling”VirtualTable only renders the rows visible in the viewport, plus a configurable overscan buffer (default: 5 rows above and below). This keeps DOM node count constant regardless of dataset size.
How it works
Section titled “How it works”- Scroll handler — On every scroll event,
VirtualTablecalculates the visible row range fromscrollTop,clientHeight, androwHeight. - Synchronous update — The new range is applied synchronously to prevent visual tearing during fast scrolling.
- Absolute positioning — Each row is positioned using
translateY(index × rowHeight). A container element is sized tototalCount × rowHeightto give the scrollbar correct proportions. - CSS containment — Rows use
contain: layout style paintandwill-change: transformfor optimal compositing.
onRangeChange callback
Section titled “onRangeChange callback”Every time the visible range changes, VirtualTable calls onRangeChange with { start, end }. This is the hook point for triggering page fetches — typically you pass cache.handleRangeChange from useTableCache:
<VirtualTable totalCount={cache.totalCount} rowHeight={40} onRangeChange={cache.handleRangeChange}>Column resizing
Section titled “Column resizing”Columns with resizable={true} render a drag handle on their right edge. Dragging the handle resizes the column in real time.
Resize behavior
Section titled “Resize behavior”- Min/max constraints —
minWidth(default: 50px) andmaxWidthclamp the drag range. - Container-aware — The column cannot be resized wider than the container minus the minimum widths of all other columns.
- Cursor override — During a drag,
document.body.style.cursoris set tocol-resizeand restored on mouse up. frvalue calculation — On drag end, the component calculates the equivalentfrvalue of the new pixel width relative to otherfrcolumns. This is reported via theonResizeEndcallback:
onResizeEnd?: (width: number, startWidth: number, frValue: number) => void| Parameter | Description |
|---|---|
width | Final pixel width after drag |
startWidth | Pixel width when drag started |
frValue | Equivalent fr value relative to other fr columns |
The useTableColumnWidths hook consumes these callbacks to persist widths automatically.
Factory functions
Section titled “Factory functions”Factory functions let you bake in default props for sub-components. This is useful when you have a design system and want consistent styling without repeating className on every instance.
import { createTableHeader, createTableColumn, createTableBody, createTableRow,} from '@requence/table'
const Header = createTableHeader({ className: 'bg-zinc-900 text-xs uppercase text-zinc-500',})
const Column = createTableColumn({ className: 'px-4 py-2', resizable: true, minWidth: 80,})
const Body = createTableBody()
const Row = createTableRow({ className: 'border-b border-zinc-800 hover:bg-zinc-900/50',})Then use them in place of the VirtualTable.* sub-components:
<VirtualTable totalCount={100} rowHeight={40}> <Header> <Column width="2fr">Name</Column> <Column width="1fr">Role</Column> </Header>
<Body> {(index) => ( <Row> <VirtualTable.Cell className="px-4">{name}</VirtualTable.Cell> <VirtualTable.Cell className="px-4">{role}</VirtualTable.Cell> </Row> )} </Body></VirtualTable>How merging works: Factory-created components have built-in default props that are merged with instance props. ClassNames are combined via tailwind-merge, so instance classes override conflicting defaults.
Available factory functions:
| Function | Creates |
|---|---|
createTableHeader | VirtualTable.Header with defaults |
createTableColumn | VirtualTable.Column with defaults |
createTableBody | VirtualTable.Body with defaults |
createTableRow | VirtualTable.Row with defaults |
createTableSkeletonRow | VirtualTable.SkeletonRow with defaults |
createTableEmpty | VirtualTable.Empty with defaults |
createTableFooter | VirtualTable.Footer with defaults |
Empty state
Section titled “Empty state”When totalCount is 0 and an Empty sub-component is provided, VirtualTable renders the header followed by the empty state content instead of the scroll container:
<VirtualTable.Empty className="py-20 text-zinc-500"> No results found.</VirtualTable.Empty>The empty state is centered within a flex container. The header still renders above it so users can see column headings.
Skeleton rows
Section titled “Skeleton rows”When the Body render function returns null for a given index (typically because the data hasn’t been fetched yet), VirtualTable renders the SkeletonRow sub-component in its place:
<VirtualTable.SkeletonRow className="animate-pulse"> <VirtualTable.Cell className="px-4"> <div className="h-3 w-24 rounded bg-zinc-800" /> </VirtualTable.Cell> <VirtualTable.Cell className="px-4"> <div className="h-3 w-32 rounded bg-zinc-800" /> </VirtualTable.Cell></VirtualTable.SkeletonRow>Skeleton rows receive the same grid template as data rows, so the placeholder elements align with the header columns. They inherit the same positioning and height as regular data rows.