Active Foundation · Example
Older items above the viewport, without a jump
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} role="region" 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> );}