# Collapsible sidebar

> **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/).

This recipe lets the user collapse the channel list and bring it back. A button in the channel list's header collapses it, and a button in the channel header shows it again. On a phone, the list opens as an overlay over the channel and closes when the user picks a channel.

The app owns the open or closed state, and CSS does the rest. The SDK gives the buttons a place:

- `HeaderEndContent`, an override key, renders at the end of the channel list's header, and of the thread list's, while a channel or a thread is open.
- `StartContent`, a prop of `ChannelHeader`, renders at its start. It replaces the back button, so the recipe renders that button too.

Below 768px wide, the SDK's stylesheet hides the channel list, and the app shows it, for example as an overlay. That's the mobile part of this recipe. [Responsive layouts](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/responsive-layouts/) covers the rest of a layout on small screens.

## Best practices

- Keep the sidebar state in the app, in a React context, so the buttons and the layout read the same value.
- Use one toggle component in both places. It collapses the list when it's open, and shows it when it's collapsed.
- Drive the layout with a class on an element around `ChatView`. The channel list's root, `.str-chat__channel-list`, already animates its width, opacity and position.
- Hide the button in the channel header while the list is visible, so only one of the two shows.
- Close the list when a channel opens on a phone, where the list covers the channel.
- To keep the user's choice across reloads, save `sidebarOpen` in `localStorage`.

## Create sidebar state

```tsx title="src/SidebarContext.tsx"
import { createContext, useCallback, useContext, useState } from "react";
import type { PropsWithChildren } from "react";

type SidebarContextValue = {
  closeSidebar: () => void;
  openSidebar: () => void;
  sidebarOpen: boolean;
};

const SidebarContext = createContext<SidebarContextValue | undefined>(
  undefined,
);

export const useSidebar = () => {
  const value = useContext(SidebarContext);
  if (!value)
    throw new Error("useSidebar must be used within a SidebarProvider");
  return value;
};

export const SidebarProvider = ({
  children,
  initialOpen = true,
}: PropsWithChildren<{ initialOpen?: boolean }>) => {
  const [sidebarOpen, setSidebarOpen] = useState(initialOpen);

  const closeSidebar = useCallback(() => setSidebarOpen(false), []);
  const openSidebar = useCallback(() => setSidebarOpen(true), []);

  return (
    <SidebarContext.Provider value={{ closeSidebar, openSidebar, sidebarOpen }}>
      {children}
    </SidebarContext.Provider>
  );
};
```

## Create the toggle component

The toggle reads the state and flips it. `Button` is the SDK's button, so the toggle looks like the header's other buttons:

```tsx title="src/SidebarToggle.tsx"
import { Button } from "stream-chat-react";
import { useSidebar } from "./SidebarContext";

export const SidebarToggle = () => {
  const { closeSidebar, openSidebar, sidebarOpen } = useSidebar();

  return (
    <Button
      appearance="ghost"
      aria-label={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
      circular
      className="sidebar-toggle"
      onClick={sidebarOpen ? closeSidebar : openSidebar}
      size="md"
      variant="secondary"
    >
      ☰
    </Button>
  );
};
```

## Wire it into the app

Register the toggle as `HeaderEndContent`, and pass it to `ChannelHeader` as part of its `StartContent`, before the back button. A wrapper around `ChatView` gets the class that says whether the list is collapsed:

```tsx title="src/App.tsx"
import {
  ChannelHeader,
  ChannelNavigation,
  Chat,
  MessageComposer,
  MessageList,
  ThreadHeader,
  WithComponents,
  WorkspaceNavigationBackButton,
} from "stream-chat-react";
import {
  ChannelSlot,
  ChatView,
  ThreadSlot,
} from "stream-chat-react/slot-layout";
import type { LayoutDescriptor } from "stream-chat-react/slot-layout";
import { AutoCloseSidebar } from "./AutoCloseSidebar";
import { SidebarProvider, useSidebar } from "./SidebarContext";
import { SidebarToggle } from "./SidebarToggle";
import "./sidebar.css";

const layouts: LayoutDescriptor[] = [
  { id: "channels", slots: ["channel", "thread"] },
];

// Declared once, so WithComponents gets the same object on every render.
const overrides = { HeaderEndContent: SidebarToggle };

// The toggle, then the back button the header shows by default.
const ChannelHeaderStart = () => (
  <>
    <SidebarToggle />
    <WorkspaceNavigationBackButton />
  </>
);

const ChannelsView = () => (
  <>
    <ChannelNavigation />
    <ChannelSlot slot="channel">
      <ChannelHeader StartContent={ChannelHeaderStart} />
      <MessageList />
      <MessageComposer />
    </ChannelSlot>
    <ThreadSlot slot="thread">
      <ThreadHeader />
      <MessageList />
      <MessageComposer />
    </ThreadSlot>
  </>
);

const ChatLayout = () => {
  const { sidebarOpen } = useSidebar();

  return (
    <div
      className={`chat-layout ${sidebarOpen ? "" : "chat-layout--sidebar-collapsed"}`}
    >
      <ChatView layouts={layouts} views={{ channels: <ChannelsView /> }}>
        <AutoCloseSidebar />
      </ChatView>
    </div>
  );
};

export const App = () => (
  <SidebarProvider>
    <WithComponents overrides={overrides}>
      <Chat client={client}>
        <ChatLayout />
      </Chat>
    </WithComponents>
  </SidebarProvider>
);
```

`AutoCloseSidebar` is the last part of this page. The channel list comes from the paginators registered on `client.channelManager`, as in the [Quick start](https://getstream.io/chat/docs/sdk/react/v15/getting-started/quick-start/#add-a-channel-list-with-chatview).

## Add CSS for sidebar visibility

On a wide screen, the collapsed list shrinks to nothing and fades out. The SDK's stylesheet animates the change. The button in the channel header shows only while the list is collapsed:

```css title="src/sidebar.css"
.chat-layout {
  display: flex;
  height: 100%;
}

.chat-layout--sidebar-collapsed .str-chat__channel-list {
  flex-basis: 0;
  min-width: 0;
  opacity: 0;
  overflow: hidden;
  pointer-events: none;
  width: 0;
}

/* One toggle at a time: the header's shows only while the list is collapsed. */
.chat-layout:not(.chat-layout--sidebar-collapsed)
  .str-chat__channel-header
  .sidebar-toggle {
  display: none;
}
```

## Responsive behavior

Below 768px, the SDK's stylesheet hides the channel list and places it over the channel. Show it while the sidebar is open. Add this to `sidebar.css`:

```css
@media (max-width: 767px) {
  .chat-layout:not(.chat-layout--sidebar-collapsed) .str-chat__channel-list {
    pointer-events: auto;
    transform: translateX(0);
    /* Shows at once; the SDK's rule delays hiding until the list has slid out. */
    transition:
      transform var(--str-chat__channel-list-transition-duration)
        var(--str-chat__channel-list-transition-easing),
      visibility 0s;
    visibility: visible;
  }
}
```

While the list is open on a phone, it covers the channel, and its header shows the toggle that closes it. Once it's closed, the channel header shows the toggle that opens it again.

Close the list when a channel opens on a phone, so the user sees the channel. `openChannels` from `useWorkspaceNavigation()` lists the channels the layout has open:

```tsx title="src/AutoCloseSidebar.tsx"
import { useEffect } from "react";
import { useWorkspaceNavigation } from "stream-chat-react";
import { useSidebar } from "./SidebarContext";

export const AutoCloseSidebar = () => {
  const { openChannels } = useWorkspaceNavigation();
  const { closeSidebar } = useSidebar();
  const openCids = openChannels.map(({ cid }) => cid).join(",");

  useEffect(() => {
    if (openCids && window.matchMedia("(max-width: 767px)").matches) {
      closeSidebar();
    }
  }, [openCids, closeSidebar]);

  return null;
};
```

`AutoCloseSidebar` renders as a child of `ChatView`, because `useWorkspaceNavigation()` returns the layout's navigation only below it. Children of `ChatView` render in every view. [Navigation](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/navigation/) explains `useWorkspaceNavigation()`.

The thread list has the same header slot. To collapse it too, add a `threads` view with `ThreadList` and apply the same CSS to the thread list's root, `.str-chat__thread-list-container`.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/recipes/app/collapsible-sidebar/](https://getstream.io/chat/docs/sdk/react/v15/recipes/app/collapsible-sidebar/).