Expected
<ChatSidebar modal={false} …> (the default — the prop's own documentation says "a chat lives next to the work, so it should not block it") should leave the rest of the application interactive while open: no focus trap, no scroll lock, and no inert applied outside the panel.
What happens instead
With modal={false}, opening the sidebar makes the entire rest of the page non-interactive — clicks, focus, and text selection are dead everywhere outside the panel until it closes. The backdrop styling is correct (pointer-events: none unless data-modal='true'), so the page looks non-modal, which makes the freeze surprising.
Cause
ChatSidebar (v4.1.0, dist/esm/Chat/ChatSidebar.js:65-68) always renders through react-aria-components' ModalOverlay/Modal. ModalOverlayInner calls useModalOverlay unconditionally whenever open (react-aria-components/dist/private/Modal.js:106-109), and that hook (react-aria/dist/private/overlays/useModalOverlay.js:24-43) unconditionally:
- runs
ariaHideOutside([panel], { shouldUseInert: true }) — walks document.body and sets element.inert = true on every sibling of the panel (ariaHideOutside.js:35,41-49), which is the actual mechanism that freezes the app;
- traps Tab focus inside the panel (
useOverlayFocusContain);
- locks body scroll (
usePreventScroll).
The modal prop only affects backdrop paint/pointer-events and dismissal — it never reaches these react-aria side effects.
Smallest reproduction
const [open, setOpen] = useState(false);
<>
<button onClick={() => setOpen(o => !o)}>Toggle chat</button>
<ChatSidebar open={open} onClose={() => setOpen(false)} topics={[]} messages={[]} />
</>
Open the sidebar, then try to click the toggle button (or anything else on the page): every element outside the panel carries inert and ignores the click.
Why it matters
Any consumer following the component's documented default gets a modal in behavior while non-modal in appearance. In Stagehand this presents as "opening the chat blocks the whole app" (Cratis/Stagehand#748) despite the consumer passing nothing but the documented defaults — the failure is invisible in the consumer's own code, which makes it expensive to locate.
Direction (yours to decide)
When modal={false}, render the panel without the Modal/ModalOverlay primitives — a plain positioned element with the same classes/animations — reserving react-aria's overlay machinery for modal={true}. Stagehand is working around it by hand-rolling the slide-in and composing the unwrapped ChatTopicList/ChatConversation pieces.
Expected
<ChatSidebar modal={false} …>(the default — the prop's own documentation says "a chat lives next to the work, so it should not block it") should leave the rest of the application interactive while open: no focus trap, no scroll lock, and noinertapplied outside the panel.What happens instead
With
modal={false}, opening the sidebar makes the entire rest of the page non-interactive — clicks, focus, and text selection are dead everywhere outside the panel until it closes. The backdrop styling is correct (pointer-events: noneunlessdata-modal='true'), so the page looks non-modal, which makes the freeze surprising.Cause
ChatSidebar(v4.1.0,dist/esm/Chat/ChatSidebar.js:65-68) always renders through react-aria-components'ModalOverlay/Modal.ModalOverlayInnercallsuseModalOverlayunconditionally whenever open (react-aria-components/dist/private/Modal.js:106-109), and that hook (react-aria/dist/private/overlays/useModalOverlay.js:24-43) unconditionally:ariaHideOutside([panel], { shouldUseInert: true })— walksdocument.bodyand setselement.inert = trueon every sibling of the panel (ariaHideOutside.js:35,41-49), which is the actual mechanism that freezes the app;useOverlayFocusContain);usePreventScroll).The
modalprop only affects backdrop paint/pointer-events and dismissal — it never reaches these react-aria side effects.Smallest reproduction
Open the sidebar, then try to click the toggle button (or anything else on the page): every element outside the panel carries
inertand ignores the click.Why it matters
Any consumer following the component's documented default gets a modal in behavior while non-modal in appearance. In Stagehand this presents as "opening the chat blocks the whole app" (Cratis/Stagehand#748) despite the consumer passing nothing but the documented defaults — the failure is invisible in the consumer's own code, which makes it expensive to locate.
Direction (yours to decide)
When
modal={false}, render the panel without theModal/ModalOverlayprimitives — a plain positioned element with the same classes/animations — reserving react-aria's overlay machinery formodal={true}. Stagehand is working around it by hand-rolling the slide-in and composing the unwrappedChatTopicList/ChatConversationpieces.