Skip to content

[web] Support virtualizing against an external scroll element (like react-virtual's getScrollElement / virtuoso's customScrollParent) #457

Description

@andrskr

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 VirtualgetScrollElement: () => TScrollElement | null. The virtualizer attaches to whatever element you return.
  • react-virtuosocustomScrollParent: 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions