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>
);
};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
HeaderStartContentandHeaderEndContentviaWithComponents— 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.
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.