# Dialogs

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

A dialog is anything that opens on top of the UI: the actions menu on a message, the reaction selector, the attachment selector's menu, a delete confirmation. The SDK opens all of them through dialog managers. Your own menus, popups and modals can use the same system, and then they get the same focus handling, the same dismissal on Escape and outside clicks, and the same stacking.

## Dialog managers

A dialog manager keeps track of a set of dialogs: which ones exist, which are open, and in what order they opened. It renders the open ones into a portal of its own, and closes them when the user clicks outside them.

>
> **Info:** **Example**
>
> When a user opens the actions menu on a message:
>
> 1. The menu button registers a dialog on the nearest manager, the message list's.
> 2. Clicking the button opens the dialog. The manager adds it to its open dialogs and renders the menu into the list's portal, next to the button.
> 3. A click outside the menu closes it, and so does Escape.
>

The SDK creates these managers:

| Manager              | Created by                                      | Dialogs that open on it                                                                                  |
| -------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| The modal manager    | `Chat`                                          | every `GlobalModal`, including the SDK's own: delete confirmations, poll creation, channel details       |
| One per message list | each `MessageList` and `VirtualizedMessageList` | the message actions menu, the reaction selector, the list of who reacted                                 |
| One per composer     | each `MessageComposer`                          | the attachment selector menu, the notice shown when the microphone is blocked                            |
| ChatView's manager   | `ChatView`, inside its `.str-chat` root         | dialogs opened by the views' content that no closer manager claims, such as a channel-list row's actions |
| One per open modal   | each `GlobalModal`                              | menus and popups opened inside that modal, so they render above it                                       |

A dialog opens on the **nearest** manager above the component that opens it. A dialog opened inside `Chat` but outside all of the other surfaces opens on the modal manager.

The modal manager's id is exported as `modalDialogManagerId`. While it has an open dialog, `Chat` shows notifications inside the modal instead of behind it. [Displaying notifications](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/displaying-notifications/) explains how notifications are routed to panels.

### Your own manager

To give a part of your app its own manager, wrap it in `DialogManagerProvider`. Its `closeOnClickOutside` prop sets whether a click outside closes the open dialogs; it is `true` by default.

```tsx
import type { PropsWithChildren } from "react";
import { DialogManagerProvider, useChatContext } from "stream-chat-react";

export const AppToolbar = ({ children }: PropsWithChildren) => {
  const { theme } = useChatContext();

  return (
    // The SDK's CSS variables are set on `.str-chat`, so the dialogs need it as an ancestor.
    <div className={`str-chat ${theme} app-toolbar`}>
      <DialogManagerProvider closeOnClickOutside={false} id="app-toolbar">
        {children}
      </DialogManagerProvider>
    </div>
  );
};
```

A manager with an `id` is registered under it. Any component can then reach it by passing that id as `dialogManagerId` to `useDialog`, `useDialogIsOpen` or `DialogAnchor`, even from outside the manager's subtree. A manager without an `id` is reachable only from inside it. `ChatView` takes the id for its manager as its `dialogManagerId` prop.

## Opening a dialog

`useDialog({ id })` registers a dialog on the nearest manager and returns its controls:

- `open()` opens it.
- `close()` closes it.
- `toggle(closeAll?)` opens or closes it. With `closeAll` set to `true`, opening it closes the manager's other dialogs.
- `remove()` removes it from the manager. A dialog is removed for you when the component that registered it unmounts.

`useDialogIsOpen(id)` subscribes to whether the dialog is open. Use it for `aria-expanded` and anything else that renders differently while the dialog is open.

This button opens a help popup below itself:

```tsx
import { useState } from "react";
import { DialogAnchor, useDialog, useDialogIsOpen } from "stream-chat-react";

const helpDialogId = "help-dialog";

export const HelpButton = () => {
  const [button, setButton] = useState<HTMLButtonElement | null>(null);
  const dialog = useDialog({ id: helpDialogId });
  const isOpen = useDialogIsOpen(helpDialogId);

  return (
    <>
      <button
        aria-expanded={isOpen}
        onClick={() => dialog.toggle()}
        ref={setButton}
      >
        Help
      </button>
      <DialogAnchor
        id={helpDialogId}
        placement="bottom-start"
        referenceElement={button}
        trapFocus
      >
        <div className="help-dialog">
          <p>Press Enter to send a message and Shift+Enter for a new line.</p>
          <button onClick={() => dialog.close()}>Got it</button>
        </div>
      </DialogAnchor>
    </>
  );
};
```

Rendered inside `ChatView`, for example in a channel header, the popup opens on `ChatView`'s manager.

`useDialog` and `DialogAnchor` have to resolve to the same manager. Here they do, because they're called in the same component. When the button and the dialog sit under different managers, pass the same `dialogManagerId` to both.

Other hooks for the same state:

| Hook                                    | Returns                                                           |
| --------------------------------------- | ----------------------------------------------------------------- |
| `useDialogIsTopmost(id)`                | whether the dialog is the one opened last among the open ones     |
| `useOpenedDialogCount()`                | how many dialogs the nearest manager has open                     |
| `useModalDialog(id)`                    | the controls for a dialog on the modal manager                    |
| `useModalDialogIsOpen(id)`              | whether a dialog on the modal manager is open                     |
| `useDialogManager({ dialogManagerId })` | `{ dialogManager }`: the nearest manager, or the one with that id |

`useDialogIsOpen` and `useDialogIsTopmost` take a manager id as their second argument, and `useOpenedDialogCount` as `{ dialogManagerId }`, to read a manager other than the nearest one.

## Anchors

`DialogAnchor` renders a dialog next to another element, its `referenceElement`, and only while the dialog is open. It positions the dialog with [Floating UI](https://floating-ui.com/).

| Prop                            | Default  | What it does                                                                                                      |
| ------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `id`                            |          | The dialog's id, the same one passed to `useDialog`.                                                              |
| `referenceElement`              | `null`   | The element the dialog is placed against.                                                                         |
| `placement`                     | `'auto'` | A Floating UI placement such as `'top-start'`, or `'auto'`, `'auto-start'` or `'auto-end'`.                       |
| `allowFlip`                     | `true`   | Lets the dialog flip to the opposite side when it doesn't fit.                                                    |
| `offset`                        |          | The distance from the reference element: a number, or an object with `mainAxis`, `crossAxis` and `alignmentAxis`. |
| `updatePositionOnContentResize` | `false`  | Repositions the dialog when its content changes size.                                                             |
| `updateKey`                     |          | Repositions the dialog whenever this value changes.                                                               |
| `closeOnClickOutside`           |          | Overrides the manager's outside-click policy for this dialog.                                                     |
| `closeOnEscape`                 | `true`   | Closes the dialog on Escape.                                                                                      |
| `closeTransitionMs`             | `0`      | Keeps the dialog mounted this long after it closes, so a CSS exit animation can play.                             |
| `trapFocus`                     |          | Keeps focus inside the dialog, and gives it `role="dialog"` and `aria-modal`.                                     |
| `focus`                         | `true`   | Moves focus into the dialog when it opens.                                                                        |
| `dialogManagerId`               |          | Opens the dialog on the manager registered under this id instead of the nearest one.                              |

Any other prop is passed to the dialog's `div`, which has the class `str-chat__dialog-contents`. Two data attributes on it are there for CSS:

- `data-str-chat-dialog-state` is `open` while the dialog is open, and `closing` during `closeTransitionMs`.
- `data-str-chat-placement` holds the placement the dialog ended up with, for example `top-start`.

This popup stays open on outside clicks, and keeps itself mounted for 180 ms after it closes so it can fade out:

```tsx
import { useState } from "react";
import { DialogAnchor, useDialog } from "stream-chat-react";

const tipDialogId = "formatting-tip";

export const FormattingTip = () => {
  const [button, setButton] = useState<HTMLButtonElement | null>(null);
  const dialog = useDialog({ id: tipDialogId });

  return (
    <>
      <button onClick={() => dialog.toggle()} ref={setButton}>
        Formatting
      </button>
      <DialogAnchor
        className="formatting-tip"
        closeOnClickOutside={false}
        closeTransitionMs={180}
        id={tipDialogId}
        placement="top-start"
        referenceElement={button}
      >
        Wrap text in *asterisks* to make it bold.
      </DialogAnchor>
    </>
  );
};
```

```css
.formatting-tip[data-str-chat-dialog-state="closing"] {
  opacity: 0;
  transition: opacity 180ms;
}
```

## Ready-made dialogs

The SDK builds its own dialogs from a few components, and you can use them too:

- `Callout` is an anchored, dismissible popup with a close button, for hints and instructions. It takes the `DialogAnchor` props and an `onClose` callback.
- `ContextMenu` is an anchored menu with items, submenus and keyboard navigation. The message actions menu is one.
- `Prompt`, `Alert` and `Viewer` lay out the content of a modal. `Prompt` asks for input, `Alert` asks the user to confirm something risky, and `Viewer` shows content without changing it. `Prompt` and `Viewer` have `Root`, `Header`, `Body` and `Footer` parts; `Alert` has `Root`, `Header` and `Actions`.

To change how every SDK menu behaves, override `ContextMenu` through `WithComponents`. This override keeps every menu open on outside clicks:

```tsx
import type { PropsWithChildren } from "react";
import { ContextMenu, WithComponents } from "stream-chat-react";
import type { ContextMenuProps } from "stream-chat-react";

const MenuThatStaysOpen = (props: ContextMenuProps) => (
  <ContextMenu {...props} closeOnClickOutside={false} />
);

export const AppComponents = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={{ ContextMenu: MenuThatStaysOpen }}>
    {children}
  </WithComponents>
);
```

To change only what a menu renders inside it, such as its header or its submenus, override `ContextMenuContent` instead. `ContextMenu` then keeps the placement, dismissal and focus handling. [Overriding components](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/overriding-components/) explains `WithComponents`.

## Modals

`GlobalModal` renders a modal on the modal manager from `Chat`. You control it with `open`, and it calls `onClose` when the user closes it with Escape, a click on the overlay or a close button. `onCloseAttempt` can refuse a close by returning `false`. When modals stack, only the one opened last takes focus and responds to Escape.

```tsx
import { useState } from "react";
import { GlobalModal, Prompt } from "stream-chat-react";

export const RenameButton = () => {
  const [open, setOpen] = useState(false);
  const close = () => setOpen(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>Rename</button>
      <GlobalModal onClose={close} open={open}>
        <Prompt.Root>
          <Prompt.Header close={close} title="Rename channel" />
          <Prompt.Body>
            <input aria-label="Channel name" />
          </Prompt.Body>
          <Prompt.Footer>
            <Prompt.FooterControls>
              <Prompt.FooterControlsButtonSecondary onClick={close}>
                Cancel
              </Prompt.FooterControlsButtonSecondary>
              <Prompt.FooterControlsButtonPrimary onClick={close}>
                Save
              </Prompt.FooterControlsButtonPrimary>
            </Prompt.FooterControls>
          </Prompt.Footer>
        </Prompt.Root>
      </GlobalModal>
    </>
  );
};
```

Inside a modal, `useModalContext().close()` closes it from any component. The props are listed on [Modal](https://getstream.io/chat/docs/sdk/react/v15/component-reference/utilities/modal/).

The SDK renders its own modals through the `Modal` override key. Replace it to change every SDK modal at once: delete confirmations, bounced messages, poll creation and poll actions, the attachment selector's dialogs, channel details and the image gallery. The SDK passes each modal a `className` of its own, such as `str-chat__message-bounce-modal`, so keep it next to yours:

```tsx
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>
);
```

## Used in

- [Actions](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/actions/): every action menu opens as a dialog.
- [Message actions](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/message-actions/) and [Custom message actions](https://getstream.io/chat/docs/sdk/react/v15/recipes/message/custom-message-actions/), whose menu is a `ContextMenu`.

## Pitfalls

- **The button and the dialog resolve to different managers.** `useDialog` in one component and `DialogAnchor` in another, under different managers, register two separate dialogs, and the button opens one that never renders. Call both in one component, or pass the same `dialogManagerId` to both.
- **Reading `isOpen` from the controls.** The object `useDialog` returns doesn't re-render your component when the dialog opens or closes. Read the open state with `useDialogIsOpen`.
- **A reference element from `useRef`.** `ref.current` is `null` during the first render and nothing re-renders when it's set, so the dialog can end up with no element to sit next to. Keep the element in state with a callback ref, as the examples do.
- **Dialogs outside `.str-chat`.** Dialog styles read the SDK's CSS variables, which are set on `.str-chat` elements: the roots of `ChatView`, `Channel` and each modal, and the channel list's theme scope outside `ChatView`. A popup opened on the modal manager outside them renders without those variables. Give it a manager inside an element with the `str-chat` class, as in [Your own manager](#your-own-manager).
- **Outside `Chat`.** Every dialog hook needs a manager, and `Chat` provides the outermost one. Outside `Chat` and any `DialogManagerProvider`, `useDialog` throws.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/dialogs/](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/dialogs/).