Askr
UI & Components

Virtual Table

Use virtual table with the current published Askr packages.

  • @askrjs/ui/virtual-table0.0.13
  • @askrjs/themes0.0.13

How to use virtual table

Virtual Table should use the standard themed surface unless the application is deliberately implementing its own design system on top of @askrjs/ui.

  1. Import the themed component from @askrjs/themes/components; use @askrjs/ui directly only when building a custom visual system.
  2. Keep virtual table labels, state, and application actions visible at the call site instead of styling internal DOM nodes.
  3. Verify keyboard behavior, focus movement or return, disabled state, and both standard themes.
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from '@askrjs/themes/components';

<Table>
  <TableHead><TableRow><TableHeaderCell>Name</TableHeaderCell><TableHeaderCell>Status</TableHeaderCell></TableRow></TableHead>
  <TableBody>{projects.map((project) => (
    <TableRow key={project.id}><TableCell>{project.name}</TableCell><TableCell>{project.status}</TableCell></TableRow>
  ))}</TableBody>
</Table>

Purpose

Virtual Table belongs to the ui & components layer. Use it when that layer owns the lifecycle or contract; keep simpler local behavior in state or ordinary components, and move network or request authority to the server boundary.

Install and import

VirtualTable is imported from @askrjs/ui/virtual-table; @askrjs/themes has nothing shipped under a matching name, so a /virtual-table subpath or dedicated styled component isn't something you'll find. What does carry over is styling — the default theme's CSS targets [data-slot="virtual-table"] along with its header, row, and cell slots, so loading @askrjs/themes/default is enough to style it even without a themes package component to import. Virtual List follows this same setup, for what it's worth.

Live examples

VirtualTable takes rows, rowHeight, headerHeight, getKey, and columns — an array of {id, header, cellComponent, width?} — and renders a sticky header above windowed, fixed-height rows. It fits a log viewer or an admin table with thousands of records, where you need real columns rather than one line per item and can't pay the DOM cost of rendering every row up front.

Anatomy

The rendered structure is a scrollable root, a sticky header row built from your columns' header values, and a virtualized body where only rows in the visible range (plus overscan) actually mount, padded by spacer rows above and below to keep scrollbar height accurate. Each cell comes from calling column.cellComponent with {row, rowIndex, rowKey, column, selected} for that row/column pair.

State model

VirtualTable tracks the same scroll state as Virtual List (scrollTop, visibleRange, isAtTop/isAtBottom) plus row selection: selectedRowKey and selectedRowIndex. Selection can be controlled through selectedRowKey/onSelectedRowKeyChange or left uncontrolled with defaultSelectedRowKey; either way, clicking a row — or calling selectRowByKey/selectRowByIndex through apiRef — updates which row's cells render with selected: true.

Keyboard and accessibility

VirtualTable doesn't implement its own keyboard navigation between cells or rows — onRowClick and the imperative selection methods are pointer and programmatic entry points, and any keyboard behavior inside a cell comes from what cellComponent renders. Because rows outside the visible range are unmounted rather than hidden, the same caveat as Virtual List applies: assistive technology and browser find-in-page only see currently-rendered rows, not the full dataset.

Styling and tokens

@askrjs/themes/default styles the [data-slot="virtual-table"] root as a bordered, rounded surface with a fixed viewport height (28rem at the 'lg' viewport size), plus an --ak-virtual-table-min-inline-size custom property controlling the minimum table width before horizontal scrolling kicks in. The tableWidth prop accepts 'compact' to switch to a denser column layout defined by that same CSS — again, all CSS-driven against markup VirtualTable already emits, with no separate themed component providing it.

API

VirtualTableProps<Row> requires rows, rowHeight, headerHeight, getKey, and columns; optional props include overscan, selectedRowKey, defaultSelectedRowKey, onSelectedRowKeyChange, onRowClick, onScroll, viewport, tableWidth ('compact'), apiRef, and asChild. VirtualTableApi adds selection methods on top of the Virtual List-style API: getSelectedRowKey(), getSelectedRowIndex(), selectRowByKey(rowKey), selectRowByIndex(index), and clearSelection(), alongside scrollToIndex/scrollToTop/scrollToBottom/getState.

Edge cases

Like Virtual List, row height (rowHeight) and header height (headerHeight) are both fixed — variable-height content will clip or misalign the virtualization math. Column width is optional per column (width?: number | string); omit it on every column and layout falls back to normal table sizing, which can fight with fixed-rowHeight virtualization if content forces wrapping. Unstable or colliding getKey values cause the same selection- and scroll-jumping issues Virtual List has, so keep rowKey stable across data refetches.

See Virtual List for the single-column version of this same windowing approach. See Table for a non-virtualized table when your row count is small enough that windowing isn't worth the complexity. See Data Table for a styled bordered container — unrelated to VirtualTable's own built-in bordered-surface styling, but visually similar.