Examples
Every working example of active-virtual on one page. Each one shows the file that renders it.
@workspace/active-virtual 2.1.0Status unknowncontent differs from the recorded release 2.1.0baseline ce081dc3
Both examples are Client examples: the list and its measurements live in your browser.
Ten thousand rows of varying heightClient
0 of 10,000 rows in the DOM
Full source examples/virtual/long-list.tsx
// 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> );}Older items above the viewport, without a jumpClient
50 messages, from Message 100
Full source examples/virtual/keyed-prepend.tsx
// apps/docs/examples/virtual/keyed-prepend.tsx'use client';
import {useState} from 'react';
import {Button} from '@workspace/active-ui/components/primitives/button';import {useVirtualAxis} from '@workspace/active-virtual/react';
interface Message { key: string; text: string;}
function messages(from: number, count: number): Message[] { return Array.from({length: count}, (_, index) => { const n = from + index; return {key: `m${n}`, text: `Message ${n}`}; });}
/** * A keyed list: loading older messages above the viewport does not move what * is on screen, because the axis anchors on a key and compensates the scroll. */export default function KeyedPrepend() { const [container, setContainer] = useState<HTMLDivElement | null>(null); const [items, setItems] = useState(() => messages(100, 50)); const {projection, itemRef} = useVirtualAxis({ keys: items.map((item) => item.key), estimate: 36, container, // Keep the item at the top in place even when the list is scrolled to // its very start, instead of following that edge. startEdge: 'preserve', }); const byKey = new Map(items.map((item) => [item.key, item])); const first = Number(items[0]!.key.slice(1)); return ( <div className="flex flex-col gap-3"> <div className="flex flex-wrap items-center gap-2"> <Button variant="outline" disabled={first === 0} onClick={() => setItems((list) => [ ...messages(Math.max(0, first - 20), Math.min(20, first)), ...list, ]) } > Load 20 older messages </Button> <span className="text-muted-foreground text-sm" aria-live="polite"> {items.length} messages, from {items[0]!.text} </span> </div> <div ref={setContainer} className="bg-background h-60 overflow-auto rounded-md border" tabIndex={0} aria-label="Messages" 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-2 text-sm" style={{ position: 'absolute', top: 0, left: 0, width: '100%', transform: `translateY(${item.offset}px)`, }} > {byKey.get(String(item.identity))?.text} </div> ))} </div> </div> </div> );}