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 }. Use this callback to fetch data for the newly visible row indices:
<VirtualTable totalCount={totalCount} rowHeight={40} onRangeChange={({ start, end }) => { // fetch rows start..end }}>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 |
You can use these callbacks to persist column widths (e.g. to localStorage) so they survive page reloads.
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.
SVG background snapshot
Section titled “SVG background snapshot”When a SkeletonRow is provided, VirtualTable captures it as an SVG snapshot and uses it as a repeating CSS background-image on the row container. This means the skeleton pattern fills the entire scrollable area — including rows that haven’t been rendered to the DOM — without mounting thousands of placeholder elements. The snapshot is re-captured when the container width changes (e.g. after a window resize or column resize).