Chat sidebar
The thread sidebar: New chat, a filter, threads grouped by date with Pinned on top, inline rename, delete with undo, live status per thread, a 52 px rail and a resizable edge.
On this page
Installation
Usage
import { ChatSidebar, ChatSidebarAccount } from "@/components/ui/chatcn/chat-sidebar"
<ChatSidebar
threads={threads}
activeId={threadId}
onSelect={(id) => router.push(`/c/${id}`)}
onNewChat={newChat}
onRename={(id, title) => renameThread(id, title)}
onPinChange={(id, pinned) => pinThread(id, pinned)}
onDelete={(id) => deleteThread(id)}
brand="Tallyline"
footer={<ChatSidebarAccount name="Leo Marchetti" detail="Pro plan" />}
/>A thread is { id, title, updatedAt, pinned?, status? }. The sidebar sorts by updatedAt and groups into Pinned, Today, Yesterday, Previous 7 days, Previous 30 days, then one group per month. Pass grouping={{ now }} for stable demos and tests.
Deleting is optimistic: the row leaves at once and an Undo toast appears at the bottom of the sidebar. onDelete runs when the undo window ends (undoMs, 5 s by default, paused while the toast is hovered or focused). Cmd/Ctrl+Z undoes too. Set undoMs={0} to delete immediately.
Inside a ChatLayout the layout owns the collapsed state and Cmd/Ctrl+B, and on mobile the sidebar becomes the drawer content: no rail, no resize handle, and picking a thread closes the drawer.
Examples
States
Loading shows skeleton rows that match the real list. A failed load says how to fix it and offers Retry. The rail keeps New chat and Search one click away, with tooltips and their shortcuts.
Props
| Prop | Type | Notes |
|---|---|---|
threads | ChatThread[] | Sorted and grouped for you |
activeId | string | null | Sets aria-current on the row |
onSelect | (id) => void | Or getHref to render rows as links |
onNewChat | () => void | Shows New chat and binds Cmd/Ctrl+Shift+O |
onRename | (id, title) => void | Enables Rename, double-click and F2 |
onPinChange | (id, pinned) => void | Shows Pin or Unpin |
onDelete | (id) => void | Called once the undo window ends |
onShare | (id) => void | Shows Share in the row menu |
renderMenuItems | (thread) => ReactNode | Extra MenuItems before Delete |
undoMs | number | Default 5000. 0 deletes at once |
loading, error, onRetry | Skeleton and failure states | |
brand, header, footer | ReactNode | Top row, under the filter, bottom |
railHeader, railFooter | ReactNode | Icon-only versions for the rail |
search, searchPlaceholder | The filter field. On by default | |
onSearchClick | () => void | Rail Search opens your chat search instead |
collapsed, defaultCollapsed, onCollapsedChange | Rail mode | |
width, defaultWidth, onWidthChange | number | Default 260 |
minWidth, maxWidth | number | Default 240 and 288 |
resizable | boolean | Drag handle on the inline-end edge |
grouping | GroupThreadsOptions | now, locale, group labels |
hotkeys, newChatHotkey, sidebarHotkey | Default mod+shift+o and mod+b |
Keyboard
| Key | Action |
|---|---|
Cmd/Ctrl+Shift+O | New chat |
Cmd/Ctrl+B | Collapse or expand (inside ChatLayout, or standalone) |
↓ in the filter | Move to the first result |
↑, ↓, Home, End | Move between threads |
F2 | Rename the focused thread |
Delete | Delete the focused thread |
Shift+F10 | Open the row menu |
Cmd/Ctrl+Z | Undo the last delete while the toast shows |
←, → on the edge | Resize by 8 px, 24 px with Shift. Enter collapses |
Accessibility
- An
asidelabelled Sidebar holds anavlabelled Chats. Group labels are headings, rows sit in lists. - One row is in the tab order at a time (roving focus), so Tab leaves the list in one step. Tab from a row reaches its menu button.
- Status marks are never color alone: a spinner, a hand, an alert circle or a dot, each with a spoken label.
- The resize edge is a
separatorwitharia-valuenowin px. The Undo toast is a polite status region. - Deleting the focused thread moves focus to its neighbour.
Motion
- Row hover fills in 150 ms. The row menu button appears instantly, since rows are hovered all day.
- A generated title replacing New chat fades in once over 200 ms.
- The Undo toast rises over 400 ms on the plain ease. Collapsing with the pointer fades the content in over 150 ms; from the keyboard it is instant. Width never animates.