active-virtual
Layout-stable virtualization - a headless core that decides which items of a long, changing collection belong on screen, with a browser and a React adapter.
@workspace/active-virtual 2.1.0Status unknowncontent differs from the recorded release 2.1.0baseline ce081dc3
@workspace/active-virtual decides which items of a long, mutable collection belong on screen,
where they go, and what keeps its place when items are measured, inserted, removed or reordered.
You render the items; the package renders nothing. A browser adapter observes the scroll container
and carries out the scroll writes, and a React hook binds one axis.
Ten thousand rows of varying heightClient
0 of 10,000 rows in the DOM
// apps/docs/examples/virtual/long-list.tsx'use client';
import {useState} from 'react';
import {Button} from '@workspace/active-ui/components/primitives/button';import {Input} from '@workspace/active-ui/components/primitives/input';import {Label} from '@workspace/active-ui/components/primitives/label';import {useVirtualAxis} from '@workspace/active-virtual/react';
const COUNT = 10_000;
/** Every seventh row wraps to a second line: the axis measures real heights. */function rowText(position: number): string { const base = `Row ${(position + 1).toLocaleString('en')}`; return position % 7 === 3 ? `${base}: a longer note that wraps to a second line in a narrow column, so this row is taller than its estimate.` : base;}
/** 10,000 rows of varying height; only the rows in view (plus overscan) are in the DOM. */export default function LongList() { const [container, setContainer] = useState<HTMLDivElement | null>(null); const [target, setTarget] = useState('5000'); const {projection, controller, itemRef} = useVirtualAxis({ count: COUNT, estimate: 32, container, }); return ( <div className="flex flex-col gap-3"> <form className="flex flex-wrap items-end gap-2" onSubmit={(event) => { event.preventDefault(); const row = Number(target); if (Number.isInteger(row) && row >= 1 && row <= COUNT) controller.scrollToPosition(row - 1, {align: 'center'}); }} > <div className="flex flex-col gap-1.5"> <Label htmlFor="virtual-target">Row</Label> <Input id="virtual-target" className="w-28" inputMode="numeric" value={target} onChange={(event) => setTarget(event.target.value)} /> </div> <Button type="submit" variant="outline"> Go to row </Button> </form> <div ref={setContainer} className="bg-background h-72 overflow-auto rounded-md border" tabIndex={0} aria-label="Ten thousand rows" data-virtual-container > <div style={{height: projection.extent, position: 'relative'}}> {projection.items.map((item) => ( <div key={item.identity} ref={itemRef(item.identity)} className="border-b px-3 py-1.5 text-sm" style={{ position: 'absolute', top: 0, left: 0, width: '100%', transform: `translateY(${item.offset}px)`, }} data-position={item.position} > {rowText(item.position)} </div> ))} </div> </div> <p className="text-muted-foreground text-sm" aria-live="polite"> {projection.items.length} of {COUNT.toLocaleString('en')} rows in the DOM </p> </div> );}What it gives you#
- Headless, one axis at a time. No components and no styling. A grid is two axes on one scroll container.
- Identity. Positional (the position is the identity) or keyed (a key stays with its item through every change). Measurements belong to the identity.
- Estimates now, measurements later. Items are measured through their element, and measured again when they grow or shrink.
- Anchoring. Every geometry change keeps an anchor in place: an edge, a keyed item or the focused item. Older items loaded above the viewport do not move what is on screen.
- Scroll requests. To an item, an offset, a delta or an edge; the latest request wins, a request to an item converges as its surroundings are measured, and a user scroll takes over.
- Platform. Element or window scrolling, right to left on horizontal axes, server rendering with a seeded first window.
What it does not do#
- It renders nothing and styles nothing: the markup, the positioning and the accessibility of the list are yours.
- It does not fetch or page data; it virtualizes a collection whose count or keys you give it.
- Vertical writing modes and nested scroll containers are not proven.
Entry points#
| Import | Contents |
|---|---|
@workspace/ | create, create, find, and every type |
@workspace/ | use, inspect, and the types of the hook |
What your application provides#
- A scroll container with a fixed or bounded size and
overflow: auto, and the inner element ofprojection.extentthat gives the content its length. - The rendering: one element per projected item, positioned at
item.offset, with theitemRefthat lets the item be measured. - Accessibility: a list of 10,000 rows has 10,000 rows for a screen reader only if you say so
(
aria-rowcount,aria-setsizeandaria-posinset, or a different design); the package keeps focus in place when you enablefocusCustody. - React 19 for
./react; nothing for the root entry.
Dependencies#
None at run time. react and react-dom are optional peers, needed only for ./react.
active-grid uses this package for its row and column virtualization.