Modal
GlobalModal renders a modal on the modal dialog manager that Chat provides, and the SDK's own modals go through it. Modals explains how it fits with the other dialogs.
Best practices
- Render it inside
Chat: the modal dialog manager comes from there. - Control it with one
openstate, and set that state tofalseinonClose. - Use
onCloseAttemptto keep the modal open, for example while a form has unsaved changes. - Give it a label: pass
aria-labeloraria-labelledby, or use the SDK'sPrompt,AlertorViewer, whose header labels it. - To change every SDK modal at once, replace the
Modaloverride key, and keep theclassNamethe SDK passes.
Basic usage
import { useCallback, useState } from "react";
import { GlobalModal } from "stream-chat-react";
export const Example = () => {
const [open, setOpen] = useState(false);
const closeModal = useCallback(() => setOpen(false), []);
return (
<>
<button onClick={() => setOpen(true)} type="button">
Open modal
</button>
<GlobalModal aria-label="Example" onClose={closeModal} open={open}>
<div className="custom-modal-body">Custom content</div>
</GlobalModal>
</>
);
};The user closes it with Escape, a click on the overlay, or the close button passed as CloseButtonOnOverlay. Each calls onCloseAttempt, then onClose. When modals stack, only the one opened last responds and holds the focus. Focus returns where it was when the modal closes.
Inside the modal, useModalContext() returns close, which closes it the same way as the close button, and dialogId. Outside a modal, close does nothing.
SDK modal overrides
The SDK renders its own modals through the Modal override key: delete confirmations, bounced messages, poll creation and poll actions, the attachment selector's dialogs, channel details and the image gallery. A component registered with WithComponents replaces every one of them in that subtree. It receives ModalProps, including a className of the SDK's own, such as str-chat__message-bounce-modal, so keep it next to yours:
import type { PropsWithChildren } from "react";
import {
Channel,
GlobalModal,
MessageComposer,
WithComponents,
} from "stream-chat-react";
import type { ModalProps } from "stream-chat-react";
const AppModal = ({ className, ...props }: PropsWithChildren<ModalProps>) => (
<GlobalModal {...props} className={`app-modal ${className ?? ""}`} />
);
export const App = () => (
<WithComponents overrides={{ Modal: AppModal }}>
<Channel channel={channel}>
<MessageComposer />
</Channel>
</WithComponents>
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
open |
boolean |
required | Whether the modal is open. |
onClose |
(event: ModalCloseEvent) => void |
Called when the modal closes. Set open to false here. |
|
onCloseAttempt |
(source: ModalCloseSource, event: ModalCloseEvent) => boolean |
Called before closing. Return false to keep the modal open. source is "overlay", "button" or "escape". |
|
children |
ReactNode |
The modal's content. | |
className |
string |
Added to the overlay, next to str-chat, the theme from Chat and str-chat__modal. |
|
CloseButtonOnOverlay |
ComponentType<ComponentProps<"button">> |
A close button rendered on the overlay, outside the dialog. It receives onClick and ref. |
|
aria-label |
string |
The dialog's accessible name. Ignored when aria-labelledby is passed. |
|
aria-labelledby |
string |
"<dialogId>-title" |
The id of the element that names the dialog. Without it or aria-label, the title of a Prompt, Alert or Viewer header inside the modal names it. |
aria-describedby |
string |
"<dialogId>-description" |
The id of the element that describes the dialog. |
role |
"dialog" | "alertdialog" |
"dialog" |
The dialog's role. |
initialFocusStrategy |
"dialog" | "firstElement" |
"firstElement" |
What takes the focus when the modal opens: its first focusable element, or the dialog itself. With "dialog", Enter on the dialog focuses its [data-autofocus] element. |
getInitialFocusElement |
(dialogElement: HTMLDivElement) => HTMLElement | null |
Picks the element to focus when the modal opens, instead of initialFocusStrategy. Returning null focuses the dialog. |
|
dialogId |
string |
a generated id | The modal's id on the modal dialog manager. The default ids of aria-labelledby and aria-describedby are built from it. |
dialogRootProps |
ComponentProps<"div"> without aria-label, aria-labelledby, aria-describedby and role |
Props for the dialog element inside the overlay. Its className is added to str-chat__modal__dialog, and its onKeyDown runs before the modal's own. |
ModalCloseEvent is KeyboardEvent, React.KeyboardEvent, or React.MouseEvent on a button or div. useModalContext().close() passes an empty event.
Examples
Intercept close attempts
This modal closes only through its button or Escape, not a click on the overlay:
import { GlobalModal } from "stream-chat-react";
import type { ModalCloseSource, ModalProps } from "stream-chat-react";
import type { PropsWithChildren } from "react";
const onCloseAttempt = (source: ModalCloseSource) => source !== "overlay";
export const ConfirmingModal = (props: PropsWithChildren<ModalProps>) => (
<GlobalModal {...props} onCloseAttempt={onCloseAttempt} />
);Focus a field when the modal opens
import { GlobalModal } from "stream-chat-react";
const focusNameField = (dialog: HTMLDivElement) =>
dialog.querySelector<HTMLElement>("input[name='name']");
export const RenameModal = ({
onClose,
open,
}: {
onClose: () => void;
open: boolean;
}) => (
<GlobalModal
aria-label="Rename"
getInitialFocusElement={focusNameField}
onClose={onClose}
open={open}
>
<input aria-label="Name" name="name" />
</GlobalModal>
);