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.
rowGapnumber0Gap between rows in pixels. When set, rows are spaced rowHeight + rowGap pixels apart while each row remains rowHeight tall. The gap area is transparent, showing the container background.
overscannumber5Number of extra rows rendered above and below the viewport.
onRangeChange(range: { start: number; end: number }) => voidCalled when the visible row range changes. Use this to trigger data fetches for the newly visible indices.
classNamestring | ((columns) => string)Additional class name for the outer container. Can be a callback that receives the parsed column definitions.
styleCSSProperties | ((columns) => CSSProperties)Additional inline styles for the outer container. Can be a callback that receives the parsed column definitions.
aria-labelstringAccessible label for the table element.
childrenReactNodeSub-components (Header, Body, SkeletonRow, Empty, Footer).
adjustScrollPositionbooleantrueWhether the imperative scrollBy handle adjusts scroll position. Set to false to disable programmatic scroll correction.

| onScroll | (scrollTop: number, scrollHeight: number) => void | — | Called on every scroll event with the current scroll position and total scrollable height. | | ref | React.Ref<VirtualTableHandle> | — | Ref to access the imperative handle. See VirtualTableHandle. |

VirtualTable accepts an optional generic type parameter that flows through to the column definitions received by className and style callbacks:

<VirtualTable<{ transparent?: boolean }>
style={(columns) => {
// columns[n].transparent is typed as boolean | undefined
const inset = columns
.filter((c) => c.transparent)
.reduce((sum, c) => sum + (typeof c.width === 'number' ? c.width : 0), 0)
return { '--row-bg-inset': `${inset}px` } as CSSProperties
}}
>

The imperative handle exposed via ref. Useful for programmatic scroll correction when items are added or removed above the viewport.

import type { VirtualTableHandle } from '@requence/table'
PropertyTypeDescription
scrollTopnumber (readonly)Current scroll position in pixels.
scrollTo(px: number) => voidScroll to an absolute pixel offset.
scrollBy(px: number) => voidAdjust the scroll position by the given number of pixels. Gated by the adjustScrollPosition prop.

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.
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' })

createTableColumn accepts an optional generic type parameter for custom column props. Extra props are forwarded through to the column definitions and accessible in VirtualTable’s className / style callbacks:

const Column = createTableColumn<{ transparent?: boolean }>({
resizable: true,
minWidth: 80,
})
// Then in JSX:
<Column width={88} transparent />

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.