Skip to content

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

@sdasneogi

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
    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