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
Section titled “Import”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>VirtualTable Props
Section titled “VirtualTable Props”| Prop | Type | Default | Description |
|---|---|---|---|
totalCount | number | — | Required. Total number of rows in the dataset. |
rowHeight | number | — | Required. Fixed height of each row in pixels. |
rowGap | number | 0 | Gap 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. |
overscan | number | 5 | Number of extra rows rendered above and below the viewport. |
onRangeChange | (range: { start: number; end: number }) => void | — | Called when the visible row range changes. Use this to trigger data fetches for the newly visible indices. |
className | string | ((columns) => string) | — | Additional class name for the outer container. Can be a callback that receives the parsed column definitions. |
style | CSSProperties | ((columns) => CSSProperties) | — | Additional inline styles for the outer container. Can be a callback that receives the parsed column definitions. |
aria-label | string | — | Accessible label for the table element. |
children | ReactNode | — | Sub-components (Header, Body, SkeletonRow, Empty, Footer). |
adjustScrollPosition | boolean | true | Whether 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 }}>VirtualTableHandle
Section titled “VirtualTableHandle”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'| Property | Type | Description |
|---|---|---|
scrollTop | number (readonly) | Current scroll position in pixels. |
scrollTo | (px: number) => void | Scroll to an absolute pixel offset. |
scrollBy | (px: number) => void | Adjust the scroll position by the given number of pixels. Gated by the adjustScrollPosition prop. |
Sub-Components
Section titled “Sub-Components”VirtualTable.Header
Section titled “VirtualTable.Header”Wraps the column definitions. Renders as a sticky header row.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional class name for the header row group. |
children | ReactNode | — | One or more VirtualTable.Column elements. |
VirtualTable.Column
Section titled “VirtualTable.Column”Defines a single column’s width and header content.
| Prop | Type | Default | Description |
|---|---|---|---|
width | number | string | '1fr' | Column width. A number is treated as pixels. A string is used as a flexible unit (e.g. '1fr', '2fr'). |
className | string | — | Additional class name for the header cell. |
resizable | boolean | false | Whether the column can be resized by dragging a handle on its right edge. |
minWidth | number | 50 | Minimum width in pixels during resize. |
maxWidth | number | — | Maximum width in pixels during resize. |
onResizeStart | () => void | — | Called when a resize drag starts. |
onResizeEnd | (width: number, startWidth: number, frValue: number) => void | — | Called when a resize drag ends. See Column Resizing. |
children | ReactNode | — | Header cell content. |
Column Width Types
Section titled “Column Width Types”- 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
minWidthis set, the column will not collapse below that size.
Column Resizing
Section titled “Column Resizing”When resizable is true, a drag handle appears on the column’s right edge. During the drag:
- The column width is updated in real time.
- The width is clamped between
minWidthandmaxWidth(and the available container width). onResizeStart()is called when dragging begins.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 equivalentfrvalue relative to the other flexible columns. Use this when you want to persist proportional widths rather than fixed pixels.
VirtualTable.Body
Section titled “VirtualTable.Body”Provides the render function for each row.
| Prop | Type | Default | Description |
|---|---|---|---|
children | (index: number) => ReactNode | null | — | Required. 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). |
VirtualTable.Row
Section titled “VirtualTable.Row”Wrapper element for a data row. Extends all standard div props.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional class name merged onto the row. |
style | CSSProperties | — | Additional inline styles. |
...rest | ComponentProps<'div'> | — | All other div props are forwarded (e.g. onClick, onContextMenu). |
VirtualTable.Cell
Section titled “VirtualTable.Cell”A single cell within a row. Extends all standard div props.
| Prop | Type | Default | Description |
|---|---|---|---|
showOnHover | boolean | — | When true, cell content is hidden by default and only appears when the row is hovered. Useful for action buttons. |
colSpan | number | — | Number of columns this cell spans. Sets grid-column: span N. |
className | string | — | Additional class name for the cell. |
...rest | ComponentProps<'div'> | — | All other div props are forwarded. |
VirtualTable.SkeletonRow
Section titled “VirtualTable.SkeletonRow”Placeholder row rendered when Body.children returns null for an index. Extends all standard div props.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional class name for the skeleton row. |
children | ReactNode | — | Skeleton content (e.g. shimmer elements). |
...rest | ComponentProps<'div'> | — | All other div props are forwarded. |
VirtualTable.Empty
Section titled “VirtualTable.Empty”Shown when totalCount is 0. Rendered inside a centered flex container below the header.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional class name for the empty state container. |
children | ReactNode | — | Empty state content. |
VirtualTable.Footer
Section titled “VirtualTable.Footer”Rendered below the scrollable area when totalCount > 0. Receives the current visible range.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional class name for the footer container. |
children | (range: { start: number; end: number }) => ReactNode | — | Required. Render function that receives the visible range. |
Factory Functions
Section titled “Factory Functions”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.