new·Earn with mozg — 20% of every monthSend somebody here and take a fifth of every plan payment they make, for as long as they keep paying — not a bounty on the first invoice. Your handle is the link, the window is thirty days, and the commission lands on your balance the second they pay. Free to join: if you have signed in, you already have the link. mozg.sh/earnall news →
mozg.beta
Sign in

shadcn/ui · Components · all subjects

message-scroller/advanced features

6 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

MessageScroller - virtualization pattern with @tanstack/react-virtual

When a transcript is large enough to need virtualization, use MessageScrollerViewport as the scroll element and let the virtualizer own the rows. Example using @tanstack/react-virtual: ```tsx import * as React from "react" import { useVirtualizer } from "@tanstack/react-virtual" function VirtualizedTranscript({ messages, }: { messages: Array<{ id: string; content: React.ReactNode }> }) { const viewportRef = React.useRef<HTMLDivElement>(null) const virtualizer = useVirtualizer({ count: messages.length, getScrollElement: () => viewportRef.current, estimateSize: () => 86, getItemKey: (index) => messages[index]?.id ?? index, overscan: 8, }) return ( <MessageScrollerProvider> <MessageScroller> <MessageScrollerViewport ref={viewportRef}> <MessageScrollerContent className="block min-h-full"> <div className="relative w-full" style={{ height: virtualizer.getTotalSize() }} > {virtualizer.getVirtualItems().map((virtualItem) => { const message = messages[virtualItem.index] if (!message) { return null } return ( <div key={virtualItem.key} ref={virtualizer.measureElement} data-index={virtualItem.index} className="absolute start-0 top-0 w-full" style={{ transform: `translateY(${virtualItem.start}px)`, }} > <Message>{message.content}</Message> </div> ) })} </div> </MessageScrollerContent> </MessageScrollerViewport> <MessageScrollerButton /> </MessageScroller> </MessageScrollerProvider> ) } ```

MessageScroller performance characteristics

MessageScroller keeps the scroll hot path outside of React state: no React rerenders for transcript rows, no forced layout on every scroll, and minimal off-screen paint work. Scroll position, anchoring, and follow-output are tracked imperatively and mirrored onto the root and viewport through data-* attributes. MessageScrollerItem ships with content-visibility: auto and contain-intrinsic-size. Rows stay in the DOM for selection, copy, find-in-page, SSR, and assistive tech, but the browser can skip rendering work for rows far outside the viewport.

MessageScroller performance range

MessageScroller is comfortable for hundreds to low thousands of turns, including messages with markdown and composed components.

Virtualization with MessageScrollerViewport

Virtualization is intentionally left outside the primitive. When a transcript is large enough to need virtualization, use MessageScrollerViewport as the scroll element and let the virtualizer own the rows. MessageScroller renders real DOM rows and stays fast well into thousands of turns, so most transcripts never need it.

Virtualization example with @tanstack/react-virtual

Example of using MessageScroller with @tanstack/react-virtual: const viewportRef = React.useRef<HTMLDivElement>(null); const virtualizer = useVirtualizer({ count: messages.length, getScrollElement: () => viewportRef.current, estimateSize: () => 86, getItemKey: (index) => messages[index]?.id ?? index, overscan: 8 }); Then use MessageScrollerViewport ref={viewportRef} and render virtual items with virtualizer.getVirtualItems().map() inside a container with style={{ height: virtualizer.getTotalSize() }} and each item with transform: `translateY(${virtualItem.start}px)`.

What libraries does MessageScroller use

MessageScroller's behavior comes from the @shadcn/react package.

Give your agent this brain