Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

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 open state, and set that state to false in onClose.
  • Use onCloseAttempt to keep the modal open, for example while a form has unsaved changes.
  • Give it a label: pass aria-label or aria-labelledby, or use the SDK's Prompt, Alert or Viewer, whose header labels it.
  • To change every SDK modal at once, replace the Modal override key, and keep the className the 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>
);