# Collapsible Sidebar

This guide shows how to implement a collapsible sidebar layout where the channel list can be toggled open and closed.

The SDK provides two header slots via `ComponentContext` — `HeaderStartContent` (rendered in content headers like `ChannelHeader`) and `HeaderEndContent` (rendered in sidebar headers like `ChannelListHeader`). Your app owns the sidebar open/close state and provides a toggle button through these slots.

## Best Practices

- Own sidebar state at the app level with a React context or state management solution.
- Provide a single toggle component to both `HeaderStartContent` and `HeaderEndContent` via `WithComponents` — define once, appears in all headers.
- Drive sidebar visibility with CSS classes derived from your state (transforms + transitions for smooth animation).
- Handle mutual exclusivity with CSS: hide the expand toggle when the sidebar is visible, and let the sidebar's own collapse toggle disappear naturally when the sidebar is hidden.
- On mobile, auto-close the sidebar when a channel is selected.
- Persist the sidebar preference (e.g., localStorage) so it survives page reloads.

## Create Sidebar State

Create a React context that manages the sidebar open/close state. Components anywhere in the tree can read and toggle it.

```tsx
import { createContext, useCallback, useContext, useState } from "react";
import type { PropsWithChildren } from "react";

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

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 sidebar state and renders a button. The same component works in both header positions — it toggles open when closed and closed when open.

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

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

Wrap your app with `SidebarProvider`, register the toggle via `WithComponents`, and apply a CSS class based on `sidebarOpen` to your layout container.

```tsx
import {
  Channel,
  ChannelHeader,
  ChannelList,
  Chat,
  MessageComposer,
  MessageList,
  Thread,
  Window,
  WithComponents,
} from "stream-chat-react";
import { SidebarProvider, useSidebar } from "./SidebarContext";

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

  return (
    <div
      className={`chat-layout ${!sidebarOpen ? "chat-layout--sidebar-collapsed" : ""}`}
    >
      <div className="chat-sidebar">
        <ChannelList />
      </div>
      <div className="chat-main">
        <Channel>
          <Window>
            <ChannelHeader />
            <MessageList />
            <MessageComposer />
          </Window>
          <Thread />
        </Channel>
      </div>
    </div>
  );
};

const App = () => (
  <SidebarProvider>
    <WithComponents
      overrides={{
        HeaderStartContent: SidebarToggle,
        HeaderEndContent: SidebarToggle,
      }}
    >
      <Chat client={chatClient}>
        <ChatLayout />
      </Chat>
    </WithComponents>
  </SidebarProvider>
);
```

## Add CSS For Sidebar Visibility

The layout uses flexbox. The `--sidebar-collapsed` modifier collapses the sidebar panel with a smooth transition. A CSS rule hides the expand toggle in `ChannelHeader` when the sidebar is already visible.

```css
.chat-layout {
  display: flex;
  height: 100%;
}

.chat-sidebar {
  width: 300px;
  min-width: 280px;
  flex-shrink: 0;
  overflow: hidden;
  transition:
    width 180ms ease,
    min-width 180ms ease,
    opacity 180ms ease;
}

.chat-layout--sidebar-collapsed .chat-sidebar {
  width: 0;
  min-width: 0;
  opacity: 0;
  pointer-events: none;
}

.chat-main {
  flex: 1;
  min-width: 0;
}

/* Hide the expand toggle in ChannelHeader when sidebar is visible */
.chat-layout:not(.chat-layout--sidebar-collapsed)
  .str-chat__channel-header
  .sidebar-toggle {
  display: none;
}
```

On mobile, you may prefer the sidebar to overlay the content instead of pushing it:

```css
@media (max-width: 767px) {
  .chat-sidebar {
    position: absolute;
    inset: 0;
    z-index: 1;
    width: 100%;
    min-width: 0;
    transform: translateX(-100%);
    transition: transform 180ms ease;
  }

  .chat-layout:not(.chat-layout--sidebar-collapsed) .chat-sidebar {
    transform: translateX(0);
  }
}
```

## Responsive Behavior

To auto-close the sidebar when a channel is selected on mobile, watch the active channel and close the sidebar when it changes:

```tsx
import { useEffect } from "react";
import { useChatContext } from "stream-chat-react";
import { useSidebar } from "./SidebarContext";

const MOBILE_BREAKPOINT = 768;

const AutoCloseSidebar = () => {
  const { channel } = useChatContext();
  const { closeSidebar } = useSidebar();

  useEffect(() => {
    if (
      channel &&
      typeof window !== "undefined" &&
      window.innerWidth < MOBILE_BREAKPOINT
    ) {
      closeSidebar();
    }
  }, [channel?.cid, closeSidebar]);

  return null;
};
```

Render `<AutoCloseSidebar />` inside your `ChatLayout` so it has access to both contexts.


---

This page was last updated at 2026-04-13T07:27:01.022Z.

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