Skip to content

Tree rendered inside Virtualizer from react-aria-components does not correctly virtualize when mounted inside a shadow root #10093

Description

@sdasneogi

Provide a general summary of the issue here

Tree rendered inside Virtualizer from react-aria-components does not correctly virtualize when mounted inside a shadow root via ReactDOM.createRoot. The initial rendered range can be incorrect, and scrolling the tree container does not reliably update the rendered items.

This appears to be a regression in react-aria-components@1.17.0 / react-aria@3.48.0. The same reproduction works as expected in react-aria-components 1.16.x.

🤔 Expected Behavior?

A virtualized Tree inside a shadow root should behave the same as it does in the light DOM:

Initial render should start from the top of the tree.
Scrolling the tree container should update the virtualized item range.
Later items should render as the user scrolls.
Tree selection / expansion behavior should remain interactive.

😯 Current Behavior

When Tree is wrapped in Virtualizer and rendered inside a shadow root:

The rendered item range can be incorrect.
Scrolling the tree container does not reliably update the virtualized rows.
The scroll position changes, but the rendered items can remain stuck.
Non-virtualized tree behavior works, so the issue appears isolated to the virtualizer / scroll tracking path inside shadow DOM.

💁 Possible Solution

This was generated by LLM

root cause:
react-aria-components@1.17.0 introduced "Window scrolling in Virtualizer" and hardcoded allowsWindowScrolling: true in its private CollectionRoot component (the implementation behind the public Virtualizer export). This activates a fundamentally different visible-rect computation:

1.16.0 (working):

// Visible rect = just the scroll container's own position
new Rect(scrollLeft, scrollTop, clientWidth, clientHeight)
1.17.0 (broken):

// Visible rect = ancestor scroll offset + self scroll, clipped to window
new Rect(
viewportOffset.x + scrollPosition.x,
viewportOffset.y + scrollPosition.y,
Math.min(contentSize.width - viewportOffset.x, viewportSize.width),
Math.min(contentSize.height - viewportOffset.y, viewportSize.height) // ← clips items
)
viewportOffset is populated by a document.addEventListener('scroll', handler, true) ancestor-scroll listener. In Shadow DOM, this listener fires but nodeContains(target, ref.current) returns false (native Node.contains() can't cross shadow boundaries), so viewportOffset never updates — it stays at whatever getBoundingClientRect() returned at the initial mount position. The height formula then clips the visible rect to contentSize.height - mountY, shrinking the rendered window and leaving bottom items invisible.

The virtualizer appears to rely on scroll listeners / visible-rect calculation that are not correctly scoped to the shadow-root scroll container.

A possible fix would be to ensure virtualizer scroll handling works with shadow DOM by:

Listening directly on the virtualized scroll container, not only document.
Using shadow-DOM-aware event target handling.
Avoiding assumptions that scroll events or native event targets will behave the same after crossing a shadow boundary.
Ensuring visible rect calculation is based on the local scroll container when virtualizing inside a shadow root.

🔦 Context

We render components inside a shadow root for style isolation. After upgrading to react-aria-components@1.17.0, virtualized Tree behavior regressed inside shadow DOM.

This affects components that compose Tree + Virtualizer, including higher-level components that depend on virtualized tree/list rendering inside shadow-root portals or shadow-root app shells.

🖥️ Steps to Reproduce

https://codesandbox.io/p/devbox/funny-danny-nksfvn?workspaceId=ws_8Y7D6sLvDfpjduoiupTngc

Version

react-aria-components: 1.17.0 react-aria: 3.48.0

What browsers are you seeing the problem on?

Chrome

If other, please specify.

No response

What operating system are you using?

MacOS

🧢 Your Company/Team

athenahealth

🕷 Tracking Issue

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions