VirtualizedList from @prepared911/ui/virtualized-list is the list for thousands of rows: roster feeds, log tails, and other single-column collections. Every row stays in the DOM. Rows outside the viewport skip layout and paint through content-visibility: auto, so find-in-page, the accessibility tree, and keyboard focus still cover the whole list. estimatedItemSize reserves space for a row the browser has not laid out yet; after a row renders once, the browser remembers its real height.
Cardiac Arrestcritical
2026-10001 · 420 Pine St
Structure Firehigh
2026-10002 · 812 Oak Ave
Multi-Vehicle Collisionhigh
2026-10003 · 205 Main St
Welfare Checkmedium
2026-10004 · 140 Cedar Ln
Noise Complaintlow
2026-10005 · 950 Maple Ct
Fall Injurymedium
2026-10006 · 361 Birch Blvd
Suspicious Activitymedium
2026-10007 · 78 Elm Dr
Stalled Vehiclelow
2026-10008 · 1220 Walnut Way
Reset zoom to 100%
The figure above is the docs anatomy (viewport, row, empty state). Import the component from @prepared911/ui/virtualized-list.
When to use
Hundreds or thousands of cheap rows where mapping them all into a plain Scrollable drops frames.
Append-heavy feeds (live logs, chat) with stable row keys.
Lists that must stay searchable and keyboard-reachable, including rows the operator has not scrolled to.
When not to use
Short lists (dozens of rows). Map the rows inside Scrollable; it is simpler.
A table of records with columns, sorting, or selection. Use DataTable, which windows its rows.
A list that must drop off-screen nodes to save memory. This list skips work, not nodes.
Content guidelines
Empty states use the EmptyState pattern with a concrete next step, passed as emptyContent.
Give each row a stable getItemKey when rows insert or reorder. The default key is the index.
When the row you render is already a list item (NotificationItem, Item), pass rowElement={VirtualizedListRowElement.Presentation}. Without it each li nests in the list's own li, which breaks list semantics.
Draw hover and focus inside the row. Paint containment clips anything outside the row's border box, with a --space-4 margin for the focus ring.
Behavior and states
Row heights. Pass estimatedItemSize close to the typical row height in pixels. A wrong estimate only makes the scrollbar approximate until rows render. Variable heights need no per-index function.
Paging.onEndReached fires when the end of the list scrolls into view, and pauses while loadingRow or errorRow is showing.
Reverse order.reverseOrder pins the newest row to the visual block end, as in a chat log. Indexes still count from the first item.
Best practices
Do
Key rows by id when the list inserts or reorders.
Name the scroll region with scrollRegionLabel when the list scrolls and no row holds a focusable control.
Don't
Nest the list in a second scroll container without giving this one a bounded height.
Reach for className or style. The component does not take them.
Accessibility
Tab reaches a control in any row, including a row the browser has not laid out, and focusing it scrolls that row into view. See Accessibility.