This is beta documentation for Stream Chat React SDK v14. For the latest stable version, see the latest version (v13) .

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 ComponentContextHeaderStartContent (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.

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.

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.

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.

.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:

@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:

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.