Summary
On web, LegendList always renders and owns its own scroll container. There's no supported way to make it virtualize against a scroll element that the consumer already renders. I'd like a first-class way to hand LegendList an external scroll element — equivalent to TanStack react-virtual's getScrollElement: () => el or react-virtuoso's customScrollParent.
Motivation
We have a design-system ScrollArea (built on Base UI) that owns the scrolling element and layers on two things the native scroller can't give us:
- gradient fade edges at top/bottom, driven by the viewport's real overflow, and
- a custom overlay scrollbar (auto-hiding, themed).
Every scrollable surface in our app uses it, so they're visually consistent. We already use it with TanStack react-virtual for our virtualized "recent conversations" list — react-virtual borrows ScrollArea's viewport and everything composes cleanly:
function RecentConversationsVirtualList({ rows }) {
const viewportRef = useRef<HTMLDivElement>(null);
const rowVirtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => viewportRef.current, // ← borrows ScrollArea's element
estimateSize: () => 36,
overscan: 5,
});
return (
// ScrollArea owns the scroll element; the virtualizer rides along
<ScrollArea className="min-h-0 flex-1" scrollFade viewportRef={viewportRef}>
<div style={{ position: 'relative', height: rowVirtualizer.getTotalSize() }}>
{rowVirtualizer.getVirtualItems().map((vr) => (
<div
key={vr.key}
style={{ position: 'absolute', top: 0, transform: `translateY(${vr.start}px)`, height: vr.size }}
>
{/* row */}
</div>
))}
</div>
</ScrollArea>
);
}
We'd love to use LegendList for our chat transcript (its anchored-end / maintainVisibleContentPosition / scrollToEnd behavior is excellent and exactly what a chat feed needs). But because LegendList insists on owning its scroller, we can't drop it inside ScrollArea the way we do with react-virtual — the design-system fade edges and scrollbar can't wrap it. We're forced to either reimplement the fades on LegendList's own scroll div, or fall back to react-virtual and lose LegendList's chat ergonomics.
What I'm asking for
A web option to point LegendList at an externally-provided scroll element instead of creating its own — e.g. a scrollElementRef / getScrollElement prop:
const viewportRef = useRef<HTMLDivElement>(null);
<ScrollArea scrollFade viewportRef={viewportRef}>
<LegendList
getScrollElement={() => viewportRef.current} // ← LegendList virtualizes against this
data={messages}
renderItem={renderItem}
maintainVisibleContentPosition
initialScrollAtEnd
/* … */
/>
</ScrollArea>
LegendList would render only its content/spacer container into that element and use it for scroll math + scrollTo, rather than rendering its own overflow:auto div. This is the model react-virtual and react-virtuoso both support, and it's what makes them composable with arbitrary scroll-area wrappers.
Prior art
- TanStack Virtual —
getScrollElement: () => TScrollElement | null. The virtualizer attaches to whatever element you return.
- react-virtuoso —
customScrollParent: pass an HTMLElement and Virtuoso virtualizes against it instead of its own scroller.
Why not the existing escape hatches
useWindowScroll virtualizes against the document/window, not an arbitrary element, so it doesn't help when a styled scroll-area owns the scroll.
renderScrollComponent swaps the component LegendList renders (and is RN-oriented — it's omitted from the web prop types), but LegendList still owns/creates the scroller; it doesn't let me point it at a pre-existing external element.
getNativeScrollRef() is read-only access after LegendList made its own element.
Environment
@legendapp/list@3.0.0, web (@legendapp/list/react), React 19.
Summary
On web,
LegendListalways renders and owns its own scroll container. There's no supported way to make it virtualize against a scroll element that the consumer already renders. I'd like a first-class way to hand LegendList an external scroll element — equivalent to TanStackreact-virtual'sgetScrollElement: () => elorreact-virtuoso'scustomScrollParent.Motivation
We have a design-system
ScrollArea(built on Base UI) that owns the scrolling element and layers on two things the native scroller can't give us:Every scrollable surface in our app uses it, so they're visually consistent. We already use it with TanStack
react-virtualfor our virtualized "recent conversations" list — react-virtual borrowsScrollArea's viewport and everything composes cleanly:We'd love to use
LegendListfor our chat transcript (its anchored-end /maintainVisibleContentPosition/scrollToEndbehavior is excellent and exactly what a chat feed needs). But because LegendList insists on owning its scroller, we can't drop it insideScrollAreathe way we do with react-virtual — the design-system fade edges and scrollbar can't wrap it. We're forced to either reimplement the fades on LegendList's own scroll div, or fall back to react-virtual and lose LegendList's chat ergonomics.What I'm asking for
A web option to point LegendList at an externally-provided scroll element instead of creating its own — e.g. a
scrollElementRef/getScrollElementprop:LegendList would render only its content/spacer container into that element and use it for scroll math +
scrollTo, rather than rendering its ownoverflow:autodiv. This is the modelreact-virtualandreact-virtuosoboth support, and it's what makes them composable with arbitrary scroll-area wrappers.Prior art
getScrollElement:() => TScrollElement | null. The virtualizer attaches to whatever element you return.customScrollParent: pass anHTMLElementand Virtuoso virtualizes against it instead of its own scroller.Why not the existing escape hatches
useWindowScrollvirtualizes against the document/window, not an arbitrary element, so it doesn't help when a styled scroll-area owns the scroll.renderScrollComponentswaps the component LegendList renders (and is RN-oriented — it's omitted from the web prop types), but LegendList still owns/creates the scroller; it doesn't let me point it at a pre-existing external element.getNativeScrollRef()is read-only access after LegendList made its own element.Environment
@legendapp/list@3.0.0, web (@legendapp/list/react), React 19.