Askr
UI & Components

Virtual List

Use virtual list with the current published Askr packages.

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

How to use virtual list

Virtual List 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 list 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 List 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

VirtualList comes from @askrjs/ui/virtual-list, and @askrjs/themes doesn't offer a themed counterpart to pair with it — no /virtual-list subpath, no styled wrapper component. Styling still arrives, though: @askrjs/themes/default's CSS carries rules for [data-slot="virtual-list"] and [data-slot="virtual-list-row"], both of which VirtualList renders on its own elements. So pairing the headless component with the default theme's stylesheet gets you working styles, just without any themes-side props layer sitting on top.

Live examples

VirtualList takes items, a fixed rowHeight, a getKey function, and a rowComponent that renders one row from {item, index, rowKey, isVisible}; it mounts only the rows currently in (or near) the viewport instead of the full array. A chat log or activity feed is the canonical use case: pass followBottom (a boolean or {threshold}) to keep the view pinned to the newest item as more arrive, unless the user has scrolled up to read history.

Anatomy

VirtualList renders a single scrollable root containing an inner spacer sized to totalHeight, with only the visible (plus overscanned) rows mounted as real DOM nodes — everything above and below is represented by blank space rather than elements. You don't construct this structure yourself; you hand VirtualList your data and a row renderer and it manages the tree internally. asChild lets you merge the root's scroll listeners and props onto your own element instead of the default div.

State model

Internally VirtualList tracks scrollTop, the computed visibleRange, isAtTop/isAtBottom, whether it's currently following the bottom, and pendingUnseenCount — items that arrived while the user had scrolled away from the bottom. Use apiRef to reach VirtualListApi and imperatively call scrollToIndex, scrollToTop, scrollToBottom, getState, or setFollowBottom(false), for example to stop auto-scrolling once the user scrolls up. All of this state is internal; there's no controlled scrollTop prop you pass in yourself.

Keyboard and accessibility

Rows carry a data-visible attribute, but VirtualList doesn't manage focus or a roving tabindex — keyboard interaction is whatever your rowComponent implements for its own content, such as a focusable button inside a row getting tabbed to normally. Because off-screen rows are unmounted rather than hidden, a screen reader's virtual cursor or the browser's find-in-page won't encounter items scrolled out of view, which is inherent to any virtualized list and worth knowing before you rely on either for browsing the full dataset.

Styling and tokens

With @askrjs/themes/default loaded, the rendered [data-slot="virtual-list"] root gets a bordered, scrollable surface, and rows get hover and [data-visible="false"] styling out of the box, purely from CSS matching attributes VirtualList already produces. If you skip loading a theme, VirtualList still functions but renders with no visual styling at all.

API

VirtualListProps<Item> requires items, rowHeight, getKey, and rowComponent; optional props are overscan (a number or {before, after}), followBottom, onScroll, viewport ('lg' is currently the only value), apiRef, and asChild. VirtualListApi exposes scrollToIndex(index, alignment?), scrollToTop(), scrollToBottom(), getState(), getVisibleRange(), getItemCount(), getScrollTop(), isAtTop(), isAtBottom(), isFollowingBottom(), getPendingUnseenCount(), and setFollowBottom(followBottom).

Edge cases

rowHeight is fixed — VirtualList doesn't measure variable-height rows, so content that wraps to different heights per item will clip or leave gaps; keep row content a predictable height or pad it to fit. getKey must return stable, unique keys across renders, or scroll position and followBottom tracking can jump when items are added or removed. When followBottom is on and the user scrolls away from the bottom, new items increment pendingUnseenCount instead of forcing a scroll — read that value if you want a 'N new' indicator rather than silently losing the count.

See Virtual Table for the same windowing approach applied to multi-column tabular data instead of a single-column list. See Scroll Area if you need a styled custom scrollbar around non-virtualized scrollable content. See Table for the plain, non-virtualized tabular primitives.