import { useRef } from "react";
import { DialogAnchor, useDialog, useDialogIsOpen } from "stream-chat-react";
const dialogId = "custom-help-dialog";
const HelpMenu = () => {
const buttonRef = useRef<HTMLButtonElement | null>(null);
const dialog = useDialog({ id: dialogId });
const dialogIsOpen = useDialogIsOpen(dialogId);
return (
<>
<button
aria-expanded={dialogIsOpen}
onClick={() => dialog.toggle()}
ref={buttonRef}
>
Toggle help
</button>
<DialogAnchor
id={dialogId}
placement="top-start"
referenceElement={buttonRef.current}
trapFocus
>
<div className="custom-help-dialog">Help content</div>
</DialogAnchor>
</>
);
};Dialog Management
Dialog management in the React SDK covers anchored popups such as menus and callouts, plus full-screen modal surfaces such as GlobalModal.
Best Practices
- Use
DialogAnchorfor anchored popups andGlobalModalfor modal content. - Keep dialog IDs stable so focus and open state are preserved across renders.
- Read open state through
useDialogIsOpenfor accurate ARIA attributes. - Use the nearest dialog manager when integrating with SDK surfaces such as
AttachmentSelector, reaction selectors, and message actions. - Decide your outside-click dismissal policy at the dialog-manager level first, then override it per popup only when one surface needs different behavior.
- Keep popup content small and delegate larger flows to prompts or modals.
Anchored Dialogs
Anchored dialogs are positioned relative to a reference element and rendered through a dialog manager.
Rendering a dialog
Dialog API
useDialog() returns a dialog controller with:
open()close()toggle()remove()
Use useDialogIsOpen(id) to subscribe to open-state changes and wire aria-expanded or other UI state.
Dialog Managers
Anchored dialogs render through the nearest DialogManagerProvider. SDK surfaces such as MessageComposer, MessageList, and Chat already create the dialog-manager layers they need.
If you build a custom popup subtree outside those surfaces, add your own manager:
import { DialogManagerProvider } from "stream-chat-react";
const CustomDialogArea = ({ children }) => (
<DialogManagerProvider closeOnClickOutside={false} id="custom-dialog-manager">
{children}
</DialogManagerProvider>
);DialogManagerProvider controls the default outside-click dismissal policy for every anchored dialog in that subtree. Set closeOnClickOutside={false} when the whole subtree should stay open until code closes it explicitly.
Outside-Click And Transition Control
Use DialogAnchor props when one anchored surface needs different dismissal or exit-animation behavior than the rest of the manager subtree.
import { useRef } from "react";
import { DialogAnchor, useDialog } from "stream-chat-react";
const dialogId = "custom-help-dialog";
const HelpMenu = () => {
const buttonRef = useRef<HTMLButtonElement | null>(null);
const dialog = useDialog({ id: dialogId });
return (
<>
<button onClick={() => dialog.toggle()} ref={buttonRef}>
Toggle help
</button>
<DialogAnchor
closeOnClickOutside={false}
closeTransitionMs={180}
id={dialogId}
placement="top-start"
referenceElement={buttonRef.current}
>
<div className="custom-help-dialog">Help content</div>
</DialogAnchor>
</>
);
};Use:
closeOnClickOutsideto override the manager default for one anchored dialogcloseTransitionMsto keep the dialog mounted long enough for exit animations to finish
Callouts, Context Menus, Prompts, And Alerts
The SDK ships higher-level dialog primitives built on the same manager system:
CalloutContextMenuPromptAlert
Use them when your custom UI matches one of those patterns instead of rebuilding focus, placement, and dismissal logic yourself.
If you need to customize the SDK menu shell itself, override ContextMenu or ContextMenuContent through WithComponents:
import { ContextMenu, WithComponents } from "stream-chat-react";
import type { ContextMenuProps } from "stream-chat-react";
const CustomContextMenu = (props: ContextMenuProps) => (
<ContextMenu {...props} closeOnClickOutside={false} closeTransitionMs={180} />
);
const App = ({ children }) => (
<WithComponents overrides={{ ContextMenu: CustomContextMenu }}>
{children}
</WithComponents>
);Wrap ContextMenuContent instead when you want to customize submenu content, headers, or back-navigation rendering without replacing the anchoring and dismissal behavior from ContextMenu.
Modal Surfaces
GlobalModal is the modal primitive used for full-screen or overlay flows. Chat already mounts the modal dialog manager required by GlobalModal.
import { useCallback, useState } from "react";
import { GlobalModal } from "stream-chat-react";
const Example = () => {
const [open, setOpen] = useState(false);
const close = useCallback(() => setOpen(false), []);
return (
<>
<button onClick={() => setOpen(true)}>Open modal</button>
<GlobalModal onClose={close} open={open}>
<div className="custom-modal-body">Modal content</div>
</GlobalModal>
</>
);
};For SDK-owned modal flows such as attachment-selector dialogs or delete confirmations, override the shared Modal component with WithComponents:
import {
Channel,
GlobalModal,
MessageComposer,
WithComponents,
} from "stream-chat-react";
import type { ModalProps } from "stream-chat-react";
const CustomModal = (props: ModalProps) => (
<GlobalModal {...props} className="custom-modal-shell" />
);
const App = () => (
<WithComponents overrides={{ Modal: CustomModal }}>
<Channel>
<MessageComposer />
</Channel>
</WithComponents>
);