# Chat in modals

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

A chat rendered inside a natively presented modal needs its own `OverlayProvider`. This page 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 one `OverlayProvider` at the root of your app, around the navigator, and add another one only for a modal that is presented natively above it.
- Wrap the modal's own navigator in the nested `OverlayProvider`, not only 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 for each tab, screen or pane.

## Why a modal needs its own OverlayProvider

The message menu that opens on a long press, with the reaction picker and the message actions, renders inside the `OverlayProvider`, on top of everything it wraps. The image gallery does too. The root `OverlayProvider` 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`, or Expo Router's `Stack`, 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 menu behind the modal, where the user can't see it. With its own `OverlayProvider`, the menu 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. A modal with its own `OverlayProvider` works on both platforms, so the same setup serves 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 the main stack. Hide the header of the screen that presents the modal, so the only header is the inner navigator's, rendered inside the nested `OverlayProvider`:

```tsx title="App.tsx"
import { useHeaderHeight } from "@react-navigation/elements";
import { NavigationContainer } from "@react-navigation/native";
import {
  createNativeStackNavigator,
  type NativeStackScreenProps,
} from "@react-navigation/native-stack";
import { StyleSheet, Text } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import {
  Channel,
  Chat,
  type DeepPartial,
  MessageComposer,
  MessageList,
  OverlayProvider,
  type OverlayProviderProps,
  Streami18n,
  type Theme,
  useChatContext,
  useCreateChatClient,
} from "stream-chat-react-native";

const apiKey = "YOUR_API_KEY";
const userId = "YOUR_USER_ID";
const userToken = "YOUR_USER_TOKEN";

const streami18n = new Streami18n();
const theme: DeepPartial<Theme> = {
  messageList: { container: { backgroundColor: "#FAFAFA" } },
};

// The same props for every OverlayProvider in the app.
const overlayProps: OverlayProviderProps = {
  accessibility: { enabled: true },
  i18nInstance: streami18n,
  value: { style: theme },
};

type RootParamList = {
  Home: undefined;
  SupportChat: undefined;
};

type ModalParamList = {
  SupportChannel: undefined;
};

const RootStack = createNativeStackNavigator<RootParamList>();
const ModalStack = createNativeStackNavigator<ModalParamList>();

const HomeScreen = ({
  navigation,
}: NativeStackScreenProps<RootParamList, "Home">) => (
  <Text onPress={() => navigation.navigate("SupportChat")}>
    Contact support
  </Text>
);

const SupportChannelScreen = () => {
  const { client } = useChatContext();
  const headerHeight = useHeaderHeight();
  const channel = client.channel("messaging", `support-${userId}`);

  return (
    <Channel
      channel={channel}
      keyboardVerticalOffset={headerHeight}
      topInset={headerHeight}
    >
      <MessageList />
      <MessageComposer />
    </Channel>
  );
};

const SupportChat = () => (
  <OverlayProvider {...overlayProps}>
    <ModalStack.Navigator>
      <ModalStack.Screen
        component={SupportChannelScreen}
        name="SupportChannel"
        options={{ title: "Support" }}
      />
    </ModalStack.Navigator>
  </OverlayProvider>
);

const App = () => {
  const client = useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  return (
    <SafeAreaProvider>
      <GestureHandlerRootView style={styles.container}>
        {client ? (
          <OverlayProvider {...overlayProps}>
            <Chat client={client} i18nInstance={streami18n}>
              <NavigationContainer>
                <RootStack.Navigator>
                  <RootStack.Screen component={HomeScreen} name="Home" />
                  <RootStack.Screen
                    component={SupportChat}
                    name="SupportChat"
                    options={{
                      headerShown: false,
                      presentation: "fullScreenModal",
                    }}
                  />
                </RootStack.Navigator>
              </NavigationContainer>
            </Chat>
          </OverlayProvider>
        ) : (
          <Text>Connecting...</Text>
        )}
      </GestureHandlerRootView>
    </SafeAreaProvider>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1 },
});

export default App;
```

The modal renders inside the root `Chat`, so it doesn't need a `Chat` of its own. A full screen modal can't be swiped away, so give the inner navigator's header a close button. `navigation.goBack()` on the first screen of the inner stack reaches the root stack, which dismisses the modal.

## 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 the group's layout. The props come from a module shared with the root layout:

```tsx title="chat-config.ts"
import {
  type DeepPartial,
  type OverlayProviderProps,
  Streami18n,
  type Theme,
} from "stream-chat-expo";

export const streami18n = new Streami18n();

const theme: DeepPartial<Theme> = {
  messageList: { container: { backgroundColor: "#FAFAFA" } },
};

// The same props for every OverlayProvider in the app.
export const overlayProps: OverlayProviderProps = {
  accessibility: { enabled: true },
  i18nInstance: streami18n,
  value: { style: theme },
};
```

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

import { overlayProps } from "@/chat-config";

export default function ChatModalLayout() {
  return (
    <OverlayProvider {...overlayProps}>
      <Stack />
    </OverlayProvider>
  );
}
```

Then present the group as a full screen modal from the root layout, which renders the root `OverlayProvider` and `Chat` around its `Stack`:

```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`'s take precedence over the root one's and over `Chat`'s. Pass it the same `value.style`, `i18nInstance` and `accessibility` as your root `OverlayProvider`. Without them, the modal falls back to the default theme, to the default English translations and to accessibility turned off. An app that themes the SDK through `Chat`'s `style` passes the same theme to the nested `OverlayProvider`'s `value.style`.

The image gallery props, `autoPlayVideo`, `giphyVersion` and `numberOfImageGalleryGridColumns`, apply to the gallery the nested `OverlayProvider` opens, so pass those too if you set them.

Contexts that `OverlayProvider` doesn't provide, such as `Chat`'s client and `WithComponents`' components, still come from above the modal.

## 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 menu then opens inside the modal, but the modal's native header is drawn above the nested `OverlayProvider`. The menu 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.

## Choosing a presentation

The message menu 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 menu 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.

## React Native's Modal

React Native's own `Modal` component also presents its content in a separate native window, on iOS and on Android, so a chat inside it needs an `OverlayProvider` inside the `Modal`, around the chat. On Android, gestures inside a `Modal` also need a `GestureHandlerRootView` inside it; see the [`react-native-gesture-handler` installation guide](https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation).

## Multiple OverlayProviders

The message menu relies on portal hosts that are registered by name in a single native registry, shared by the whole app. So only one `OverlayProvider` owns the menu at a time: the one mounted most recently. The others keep their hosts under names of their own. 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 the one on screen.

The image gallery opens in the `OverlayProvider` closest to the image that was tapped, so a gallery opened inside the modal shows inside it.

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

A custom `MessageActions` from `WithComponents` is rendered only by the `OverlayProvider` that owns the menu, so it mounts again when ownership moves to a different `OverlayProvider`.

## Used in

- [Navigation](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/navigation/)
- [How components connect to state](https://getstream.io/chat/docs/sdk/react-native/v10/ui/how-components-connect-to-state/#overlayprovider-and-chat)
- [Overlays and bottom sheets](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/overlays-and-bottom-sheets/)

---

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