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.
- Import the themed component from @askrjs/themes/components; use @askrjs/ui directly only when building a custom visual system.
- Keep virtual list labels, state, and application actions visible at the call site instead of styling internal DOM nodes.
- 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.
Related pages
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.