Chat Sidebar
Collapsible sidebar chrome composed from slots — brand, nav, sections, rail, footer — with icon quick actions that pop on click, and the single import surface for the whole sidebar family.
Streaming markdown bugs
Drag a chat onto an edge zone, or onto another chat to reorder.
- Drag a row onto another row to reorder it.
- Drag to the top edge to pin, to the bottom edge to delete.
- Drag across sections to pin or unpin at a chosen position.
- Right-click a row to rename, pin, or delete it.
- Shift-click a range, or Ctrl/⌘-click to toggle. Pin or delete from the bar — or from the context menu, which becomes “Delete N chats”.
- The dashed “Archive” strip is a custom inline zone.
Installation
npx shadcn@latest add miskibin/chat-components/chat-sidebarAlso installs sidebar-item, sidebar-dnd, sidebar-drop-zones.
Usage
"use client"
import {
ChatSidebar,
ChatSidebarDnd,
ChatSidebarItemList,
SideActionRow,
SideIconBtn,
SidebarCollapsibleSection,
} from "@/components/ui/chat-sidebar"
export function Nav() {
const [collapsed, setCollapsed] = useState(false)
const [open, setOpen] = useState(true)
return (
<ChatSidebarDnd onDrop={handleDrop}>
<ChatSidebar
collapsed={collapsed}
onCollapsedChange={setCollapsed}
edgeZones
brand={<span className="px-1 text-sm font-medium">Chats</span>}
nav={
<SideActionRow>
<SideIconBtn label="New chat">
<Pencil className="size-4" />
</SideIconBtn>
</SideActionRow>
}
>
<SidebarCollapsibleSection
title="Recent"
open={open}
onToggle={() => setOpen((value) => !value)}
count={chats.length}
>
<ChatSidebarItemList listId="recent" items={chats} sortable />
</SidebarCollapsibleSection>
</ChatSidebar>
</ChatSidebarDnd>
)
}Examples
One import surface
chat-sidebar re-exports the whole family — the item list, the DnD provider, the zone helpers, and every type — so application code never reaches into sidebar-item, sidebar-dnd, or sidebar-drop-zones directly.
import {
ChatSidebar,
ChatSidebarDnd,
ChatSidebarItem,
ChatSidebarItemGhost,
ChatSidebarItemList,
DEFAULT_SIDEBAR_ZONES,
SideActionRow,
SideIconBtn,
SideRow,
SidebarCollapsibleSection,
SidebarDropZone,
SidebarEdgeDropZone,
SidebarEmptyState,
SidebarItemStatusDot,
SidebarResizeRail,
pinDropZone,
trashDropZone,
useSidebarDnd,
useSidebarDndList,
type ChatSidebarItemData,
type SidebarDndDrop,
type SidebarDropZoneDef,
} from "@/components/ui/chat-sidebar"Dividers are opt-in
The panel paints no rules by default — one column of rows reads as one column without them, and a line every few rows is what makes a chat sidebar look busy. dividers puts them back under the header and above the footer when the footer carries enough weight to want separating.
Widths and per-region classes
Widths are numbers, so the collapsed rail and the expanded panel are both yours to size; animateWidth eases between them (on by default). classNames reaches each region — rail, panel, header, nav, content, footer — without a single descendant selector. Collapse it below to see both widths.
Section headers as rules
rule turns a section header into a label, a hairline and a chevron — one line per working folder instead of a stack of captions. Pass action and the chips drop onto a second row so they cannot paint over a long name. live adds the dot that keeps a closed section honest while a chat inside it streams; it animates only while it is on screen and the tab is in front, so a folded sidebar full of them costs nothing. Toggle either section below.
Off-canvas on mobile
The sidebar itself never changes shape — a wrapper slides it over the conversation and a backdrop closes it. Below, absolute keeps the demo inside its frame; in a real layout use fixed with max-md: guards, exactly as app/demo/page.tsx does.
Streaming markdown bugs
Notes
API reference
ChatSidebar
collapsed*boolean- Controlled. Collapsed swaps the panel for the icon rail.
onCollapsedChange*(collapsed: boolean) => void- Fired by the header and rail toggles.
brandReact.ReactNode- Header content of the expanded panel.
navReact.ReactNode- Quick actions under the header — new chat, search. Usually SideActionRow of SideIconBtn.
railReact.ReactNode- Content of the collapsed rail. Usually SideIconBtn mirrors of nav.
childrenReact.ReactNode- The scrollable body: sections and item lists.
footerReact.ReactNode- Rendered in both the panel footer and the rail.
overlaysReact.ReactNode- Absolutely positioned extras inside the panel — a search palette, for instance.
edgeZonesboolean = false- Render the edge drop zones declared on the surrounding
ChatSidebarDndwhile a drag is in progress. dividersboolean = false- Rules under the header and above the footer. Off by default — the panel already reads as one column, and a line every few rows is what makes a chat sidebar look busy.
widthExpandednumber = 290- Panel width in pixels.
widthCollapsednumber = 60- Rail width in pixels.
widthnumber | string- Expanded width, overriding
widthExpanded. A number is pixels, a string is used verbatim. Left alone the panel readsvar(--chat-sidebar-width, widthExpanded), which is howSidebarResizeRaildrives it without a React render per frame. animateWidthboolean = true- Eases the collapse over 300ms and fades the inactive view so the swap is visible. Pass false to snap — width is a layout property, and the transition reflows everything beside the sidebar.
classNames{ rail?, panel?, header?, nav?, content?, footer? }- Per-region class overrides.
collapseLabelstring = "Collapse sidebar"- Accessible label for the collapse button.
expandLabelstring = "Open sidebar"- Accessible label for the rail button.
SideRow / SideActionRow / SideIconBtn / SidebarCollapsibleSection
SideRow{ icon?, hint? } & ComponentProps<"button">- Full-width nav row with a leading icon and a trailing hint (a shortcut, usually).
SideIconBtn{ label: string } & ComponentProps<"button">- Square icon button for quick actions, the rail and the header.
labelbecomes the title and aria-label. A click pops the glyph — no extra icon library. SideActionRowComponentProps<"div">- Horizontal cluster of SideIconBtn — New chat, Search, and the like.
SidebarCollapsibleSection{ title, open, onToggle, count?, description?, action?, rule?, live?, className?, children }- Uppercase section header with a rotating chevron, an optional count, and a slot for a header action.
ruleboolean = false- On
SidebarCollapsibleSection: label, a hairline across the rest of the row, chevron at the far end — the shape a per-folder list wants. Withactionset, the chips move onto a second row so a long label can truncate instead of colliding with them. actionReactNode- Extra header content on
SidebarCollapsibleSection— a branch, git chips, a port. Withruleit sits on a second row; otherwise at the right edge, before the count. liveboolean = false- On
SidebarCollapsibleSection: a dot on the header while the section is closed, so a folded folder still says something inside it is running. Animates only while it is on screen and the tab is in front. SidebarEmptyState{ children, className? }- Muted placeholder for an empty list — pass it as
emptyState.
Constants
SIDEBAR_WIDTH_EXPANDED290- Default panel width.
SIDEBAR_WIDTH_COLLAPSED60- Default rail width.
Data slots
Every part below is addressable from a parent, so you can restyle internals without forking the component.
chat-sidebarchat-sidebar-railchat-sidebar-panelchat-sidebar-headerchat-sidebar-navchat-sidebar-contentchat-sidebar-footersidebar-rowsidebar-icon-buttonsidebar-action-rowsidebar-sectionsidebar-section-triggersidebar-section-rulesidebar-section-livesidebar-section-action