# Chat in Modals

A chat rendered inside a natively presented modal needs its own `OverlayProvider`. This guide shows where to put it, which props to pass, and what to keep in mind when more than one `OverlayProvider` is mounted at the same time.

## Best practices

- Keep a single `OverlayProvider` at the root of your app, above `NavigationContainer`, and add another one only for modals that are presented natively above it.
- Giving such a modal its own `OverlayProvider` never hurts, on iOS or on Android. The root `OverlayProvider` keeps working and takes over again once the modal is dismissed.
- Wrap the modal's own navigator in the nested `OverlayProvider`, not just the screen.
- Pass the nested `OverlayProvider` the same props as the root one.
- Use `presentation: 'fullScreenModal'` for modals that contain a chat.
- Never mount `OverlayProvider`s side by side, for example one per tab, one per screen or one per pane.

## Why a modal needs its own `OverlayProvider`

The message overlay (the long press menu with the reaction picker and the message actions) and the image gallery render inside the `OverlayProvider`, on top of everything it wraps. The root `OverlayProvider` therefore covers every screen of your navigation stack, as long as those screens are drawn in the same native view.

On iOS, a modal of a native stack navigator (`createNativeStackNavigator` with `presentation: 'fullScreenModal'`, `'modal'`, `'pageSheet'` or `'formSheet'`) is presented in a separate view controller, on top of the root one. The root `OverlayProvider` stays behind it. A long press inside the modal hides the message and opens the overlay behind the modal, where it can't be seen. With its own `OverlayProvider`, the overlay renders inside the modal.

On Android, native stack modals are drawn in the same window as the rest of the app, so the root `OverlayProvider` already covers them.

Having its own `OverlayProvider` doesn't hurt the modal on either platform. While the modal is presented, its `OverlayProvider` owns the overlay, and once the modal is dismissed the root `OverlayProvider` takes over again, so chats outside the modal keep working as before. You can use the same setup on iOS and Android.

## Setting it up with React Navigation

Give the modal its own navigator and wrap that navigator in an `OverlayProvider`, the same way the root `OverlayProvider` wraps your main navigation stack. Hide the header of the screen that presents the modal, so the only header is the one of the inner navigator, which is rendered inside the nested `OverlayProvider`.

```tsx
import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { Chat, OverlayProvider } from "stream-chat-react-native";

const RootStack = createNativeStackNavigator();
const ModalStack = createNativeStackNavigator();

const ChatModal = () => (
  <OverlayProvider value={{ style: theme }} i18nInstance={streami18n}>
    <ModalStack.Navigator>
      <ModalStack.Screen
        name="ModalChannel"
        component={ModalChannelScreen}
        options={{ title: "Support" }}
      />
    </ModalStack.Navigator>
  </OverlayProvider>
);

export const App = () => (
  <OverlayProvider value={{ style: theme }} i18nInstance={streami18n}>
    <NavigationContainer>
      <Chat client={client} i18nInstance={streami18n}>
        <RootStack.Navigator>
          <RootStack.Screen name="Channel" component={ChannelScreen} />
          <RootStack.Screen
            name="ChatModal"
            component={ChatModal}
            options={{ headerShown: false, presentation: "fullScreenModal" }}
          />
        </RootStack.Navigator>
      </Chat>
    </NavigationContainer>
  </OverlayProvider>
);
```

`ModalChannelScreen` renders `Channel`, `MessageList` and `MessageComposer` like any other channel screen. The modal is still rendered inside the root `Chat`, so it doesn't need a `Chat` of its own.

## Setting it up with Expo Router

With Expo Router, put the modal's screens in a route group and wrap the group's `Stack` in an `OverlayProvider` in its layout:

```tsx title="app/(chat-modal)/_layout.tsx"
import { Stack } from "expo-router";
import { OverlayProvider } from "stream-chat-expo";

export default function ChatModalLayout() {
  return (
    <OverlayProvider value={{ style: theme }} i18nInstance={streami18n}>
      <Stack />
    </OverlayProvider>
  );
}
```

Then present the group as a full screen modal from the root layout:

```tsx title="app/_layout.tsx"
<Stack>
  <Stack.Screen
    name="(chat-modal)"
    options={{ headerShown: false, presentation: "fullScreenModal" }}
  />
</Stack>
```

## Passing the same props as the root `OverlayProvider`

Besides the overlay, `OverlayProvider` sets up the theme, the translations and the accessibility configuration for everything it wraps, and inside the modal the nested `OverlayProvider` takes precedence over the root one. Pass it the same `value.style`, `i18nInstance` and `accessibility` as your root `OverlayProvider`. Without them, the modal falls back to the default theme, to English and to accessibility turned off.

Contexts that `OverlayProvider` doesn't provide, such as the ones from `Chat` and `WithComponents`, are still inherited from the root of your app.

## Wrapping the navigator instead of the screen

You can also wrap only the modal's screen, for example through the `layout` prop of a `Stack.Screen`. The overlay then opens inside the modal, but the native header of the modal is drawn above the nested `OverlayProvider`. The overlay can't cover the header, and since it's positioned as if the `OverlayProvider` started at the top of the screen, it ends up shifted down by the height of the header.

Wrapping the modal's own navigator puts the header inside the `OverlayProvider` and avoids both issues.

## Choosing a presentation

The overlay is positioned as if its `OverlayProvider` started at the top left corner of the screen, which is the case for `fullScreenModal`. On iOS, `modal`, `pageSheet` and `formSheet` are presented as sheets that start below the top of the screen, so the overlay is offset by the distance between the top of the screen and the top of the sheet. Use `fullScreenModal` for modals that contain a chat.

## Multiple `OverlayProvider`s

The message overlay relies on portal hosts that are registered by name in a single native registry, shared by the whole app. Because of that, only one `OverlayProvider` can own the overlay at a time, and that is always the one mounted most recently. When the modal is presented, its `OverlayProvider` takes over. When the modal is dismissed and its `OverlayProvider` unmounts, the root `OverlayProvider` takes over again. This matches how modals behave, since the most recently presented modal is always the one on screen.

It also means that `OverlayProvider`s must not be mounted side by side, as siblings that are visible or stay mounted at the same time. Typical examples are an `OverlayProvider` per tab of a tab navigator (tabs stay mounted once visited), one per screen, or one per pane of a split view. The last mounted sibling owns the overlay even while a chat under another one is being used, so a long press opens the overlay inside the wrong `OverlayProvider`, for example in a tab that isn't visible.

A custom `MessageActions` override is rendered only by the `OverlayProvider` that owns the overlay at that moment, so it is mounted again when ownership moves to a different `OverlayProvider`.

Keep one `OverlayProvider` at the root and add nested ones only for modals that are presented above it.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/guides/chat-in-modals/](https://getstream.io/chat/docs/sdk/react-native/guides/chat-in-modals/).