# Navigation

The SDK's components don't decide where a channel or a thread opens. A row in the channel list calls `openChannel(channel)` and leaves the rest to the layout, and every component that opens, closes or highlights a channel or a thread does the same, through one interface: `useWorkspaceNavigation()`. Use this page to call that interface from your own components, to know what happens without a layout, and to provide a navigation of your own. [Workspace with ChatView](https://getstream.io/chat/docs/sdk/react/ui/layout/workspace-with-chatview/) explains the SDK's layout, and [Moving between channels, threads and messages](https://getstream.io/chat/docs/sdk/react/ui/layout/moving-between-channels-threads-and-messages/) lists every component that uses the interface. The members and options are in [Workspace navigation API](https://getstream.io/chat/docs/sdk/react/component-reference/layout/workspace-navigation-api/).

## One navigation interface

`useWorkspaceNavigation()`, from `stream-chat-react`, returns the same object anywhere below the layout:

| Member                                                             | What it does                                                                                               |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `openChannel(channel, options?)`                                   | Opens a channel                                                                                            |
| `openThread(target, options?)`                                     | Opens a thread. The target is a `Thread`, or `{ channel, message }` for the thread of that parent message. |
| `closeChannel(cid?, options?)`, `closeThread(threadId?, options?)` | Close the panel that shows the channel or the thread                                                       |
| `goBack(key?, options?)`, `canGoBack(key?, panel?)`                | Remove a channel or thread opened on top of something else, and say whether there's anything beneath it    |
| `openChannels`, `openThreads`                                      | What the panels hold, without anything opened on top of them                                               |
| `isChannelActive(cid?)`, `isThreadActive(threadId?)`               | Whether a channel or thread is on screen: open in a panel and not covered by something opened on top of it |
| `isChannelDismissable`, `isThreadDismissable`                      | Whether the panel that shows it can be closed                                                              |
| `isThreadsView`                                                    | Whether the thread list's view is the one shown                                                            |

Here is a `SupportButton` component that opens a support channel, and shows as pressed while that channel is on screen:

```tsx
import type { Channel as StreamChannel } from "stream-chat";
import { useWorkspaceNavigation } from "stream-chat-react";

export const SupportButton = ({ channel }: { channel: StreamChannel }) => {
  const { isChannelActive, openChannel } = useWorkspaceNavigation();

  return (
    <button
      aria-pressed={isChannelActive(channel.cid)}
      onClick={(event) => openChannel(channel, { event })}
      type="button"
    >
      Support
    </button>
  );
};
```

`openChannel` places a channel; it doesn't load it. A channel from a channel list is loaded by the list's query. A channel you get from `client.channel()` needs `Channel.watch()` first, as in [Show one channel](https://getstream.io/chat/docs/sdk/react/getting-started/quick-start/#show-one-channel).

## Beside and on top

Every operation takes the same options:

| Option     | What it does                                                                                                                                                                                                                                    |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `additive` | `true` opens **beside** the current content instead of replacing it. In a layout with a `channel` slot and a `thread` slot, a channel opened this way goes into the `thread` slot, next to the open channel. A layout with one slot ignores it. |
| `layer`    | `true` opens **on top** of what the panel shows, as a [layer](https://getstream.io/chat/docs/sdk/react/ui/layout/customizing-chatview/#layers). Going back removes it and shows what's beneath.                                                                     |
| `event`    | The click or key press that started the navigation. The channel list, search results and header buttons pass it, so an override can read the ⌘ or ctrl key. The SDK doesn't act on it itself.                                                   |
| `panel`    | The panel the call comes from. See [Panels](#panels).                                                                                                                                                                                           |

The two can happen together. When the slot beside the channel already shows a thread, `<ChatView/>` stacks a channel opened beside it on top of that thread. The back button in that channel's header returns to the thread. [Where an opened channel or thread lands](https://getstream.io/chat/docs/sdk/react/ui/layout/workspace-with-chatview/#where-an-opened-channel-or-thread-lands) has the full rules.

What shows depends on how the layout renders the slot. `<Slot/>` shows whatever the slot holds, layers included. `<ChannelSlot/>` shows only a channel and `<ThreadSlot/>` only a thread, without layers. So in the Quick start app, a channel opened beside or on top doesn't show. See [Rendering a slot](https://getstream.io/chat/docs/sdk/react/ui/layout/workspace-with-chatview/#rendering-a-slot).

The thread list opens a thread beside the current one on ⌘/ctrl-click, when its view has a second slot for it. The channel list passes the event but always replaces the open channel; [⌘/ctrl-click opens beside](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/ctrl-click-opens-beside/) changes that.

### Panels

A [panel](https://getstream.io/chat/docs/sdk/react/ui/layout/workspace-with-chatview/#views-and-slots) is the part of the screen that shows a slot. The same channel can be open in two panels at once, for example after the user opens it beside itself. `closeChannel(cid)` then has to know which of the two to close.

`<ChannelSlot/>`, `<ThreadSlot/>` and `<Slot/>` declare the panel they render through `<WorkspacePanelProvider/>`, and `useWorkspacePanel()` reads it. The back and close buttons in `<ChannelHeader/>` and `<ThreadHeader/>` pass it as `panel`, so each acts on its own panel. Here is a `CloseChannelButton` component that does the same:

```tsx
import {
  useChannel,
  useWorkspaceNavigation,
  useWorkspacePanel,
} from "stream-chat-react";

export const CloseChannelButton = () => {
  const channel = useChannel();
  const panel = useWorkspacePanel();
  const { closeChannel, isChannelDismissable } = useWorkspaceNavigation();

  // Only a channel opened beside another one can be closed.
  if (!isChannelDismissable(channel.cid, panel)) return null;

  return (
    <button
      onClick={(event) => closeChannel(channel.cid, { event, panel })}
      type="button"
    >
      Close
    </button>
  );
};
```

The SDK's own buttons are `<WorkspaceNavigationBackButton/>` and `WorkspaceNavigationCloseButton`. A custom header can render them to keep them.

## Without a layout

Outside `<ChatView/>` and any `<WorkspaceNavigationProvider/>`, `useWorkspaceNavigation()` returns a default that does nothing. Every open and close is ignored, `openChannels` and `openThreads` are empty, and every check returns `false`. Components call it without checking for a layout, so they still render:

- A row in the channel list does nothing when clicked, unless it has an `onSelect` handler.
- The replies button and the thread actions in the message menu do nothing.
- No row in the channel list is highlighted as open.
- The back and close buttons in the headers render nothing.
- Selecting a pinned message in channel details still jumps to the message in the channel, and **Send direct message** on a member's profile still creates the direct message channel, but neither opens the channel.

The first time each of `openChannel`, `openThread`, `closeChannel`, `closeThread` and `goBack` is called on the default, it logs a console warning that no workspace navigation is provided. The `defaultWorkspaceNavigation` export doesn't warn, so the members you leave out when you spread it into your own navigation stay silent.

An app that shows one channel needs none of this: it passes the channel to `<Channel/>` itself, as in the [Quick start](https://getstream.io/chat/docs/sdk/react/getting-started/quick-start/#show-one-channel). For navigation to do something, render [`<ChatView/>`](https://getstream.io/chat/docs/sdk/react/ui/layout/workspace-with-chatview/), or provide your own.

## Providing your own navigation

`<WorkspaceNavigationProvider/>` gives the components below it a navigation of your own. Here is a `TwoPaneChat` component that keeps the open channel and thread in React state, without `<ChatView/>`:

```tsx
import { useMemo, useState } from "react";
import { Thread as StreamThread } from "stream-chat";
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  ChannelHeader,
  ChannelNavigation,
  defaultWorkspaceNavigation,
  MessageComposer,
  MessageList,
  Thread,
  ThreadHeader,
  useChatContext,
  WorkspaceNavigationProvider,
} from "stream-chat-react";
import type { WorkspaceNavigation } from "stream-chat-react";
import { useActiveThread } from "stream-chat-react/slot-layout";

export const TwoPaneChat = () => {
  const { client } = useChatContext();
  const [channel, setChannel] = useState<StreamChannel>();
  const [thread, setThread] = useState<StreamThread>();

  // Marks the thread as on screen, so its new replies are marked read.
  useActiveThread({ activeThread: thread });

  const navigation = useMemo<WorkspaceNavigation>(
    () => ({
      // The members left out keep the defaults, which do nothing.
      ...defaultWorkspaceNavigation,
      closeThread: () => setThread(undefined),
      isChannelActive: (cid) => cid !== undefined && cid === channel?.cid,
      isThreadActive: (threadId) =>
        threadId !== undefined && threadId === thread?.id,
      isThreadDismissable: () => true,
      openChannel: (nextChannel) => {
        setChannel(nextChannel);
        setThread(undefined);
      },
      openChannels: channel ? [channel] : [],
      openThread: (target) =>
        setThread(
          target instanceof StreamThread
            ? target
            : client.threads.ensure({
                channel: target.channel,
                parentMessage: target.message,
              }),
        ),
      openThreads: thread ? [thread] : [],
    }),
    [channel, client, thread],
  );

  return (
    <WorkspaceNavigationProvider value={navigation}>
      <ChannelNavigation />
      {channel && (
        <Channel channel={channel}>
          <ChannelHeader />
          <MessageList />
          <MessageComposer />
        </Channel>
      )}
      {thread && (
        <Thread thread={thread}>
          <ThreadHeader />
          <MessageList />
          <MessageComposer />
        </Thread>
      )}
    </WorkspaceNavigationProvider>
  );
};
```

Here is what each part does:

1. A click on a row in the channel list calls `openChannel`, which shows the channel and closes the thread.
2. The replies button calls `openThread` with the channel and the parent message. `client.threads.ensure()` returns the thread for that message, creating it if it doesn't exist yet.
3. `isThreadDismissable` returns `true`, so `<ThreadHeader/>` shows its close button, which calls `closeThread`.
4. `isChannelActive` highlights the open channel's row.

Memoize the value: every component that reads it re-renders when it changes. `useActiveThread` comes from the layout's entry point but doesn't need `ChatView`.

To change how `<ChatView/>` navigates rather than replace it, pass `deriveWorkspaceNavigation` to `ChatView`. See [Changing navigation behaviour](https://getstream.io/chat/docs/sdk/react/ui/layout/customizing-chatview/#changing-navigation-behaviour).

## Used in

- [Channel lists](https://getstream.io/chat/docs/sdk/react/features/channels/channel-lists/)
- [Search](https://getstream.io/chat/docs/sdk/react/features/channels/search/)
- [Replies and threads](https://getstream.io/chat/docs/sdk/react/features/messaging/replies-and-threads/)
- [⌘/ctrl-click opens beside](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/ctrl-click-opens-beside/)

---

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