Chat layout
The app shell: sidebar, thread and an optional side panel on the shared Resizable. Below 768 px the sidebar becomes a drawer and the panel a full-screen sheet.
On this page
Installation
Usage
import { ChatHeader } from "@/components/ui/chatcn/chat-header"
import { ChatLayout } from "@/components/ui/chatcn/chat-layout"
import { ChatSidebar } from "@/components/ui/chatcn/chat-sidebar"
import { Chatbot } from "@/registry/blocks/chatbot/chatbot"
<ChatLayout
fullHeight
sidebar={<ChatSidebar threads={threads} activeId={id} onSelect={open} onNewChat={newChat} />}
panel={<ArtifactPanel {...artifact} onClose={() => setPanelOpen(false)} />}
panelOpen={panelOpen}
onPanelOpenChange={setPanelOpen}
panelLabel="Artifact"
>
<Chatbot chat={chat} header={<ChatHeader title={title} onTitleChange={rename} />} />
</ChatLayout>The layout owns the sidebar state: collapsed to the rail on desktop, open or closed as a drawer on mobile. ChatSidebar and ChatHeader read it, so the header shows the sidebar toggle only where it is needed and the drawer closes when a thread is picked. Read it in your own parts with useChatcnShellLayout().
Opening the panel never remounts the thread, so a half-typed message survives. The thread keeps at least threadMinSize; the split persists with autoSaveId.
Examples
Workspace panel
An artifact beside the thread. Drag the divider, double-click it to reset, or close the panel and the thread takes the space back.
Mobile
Forced with layout="mobile" and portalled into the frame with container. Drag the drawer back toward the edge to close it, or swipe in from the edge to open it.
Props
| Prop | Type | Notes |
|---|---|---|
children | ReactNode | The thread: header, conversation, composer |
sidebar | ReactNode | Usually a ChatSidebar |
sidebarCollapsed, defaultSidebarCollapsed, onSidebarCollapsedChange | Desktop rail | |
sidebarOpen, defaultSidebarOpen, onSidebarOpenChange | Mobile drawer | |
panel, panelOpen, onPanelOpenChange | Side panel, sheet on mobile | |
panelLabel | string | Landmark name and sheet title. Default Panel |
panelDefaultSize, panelMinSize, panelMaxSize | ResizableSize | Default 45, 320 px, 70 |
threadMinSize | ResizableSize | Default 360 px |
autoSaveId | string | Persist the split |
layout | "auto" | "desktop" | "mobile" | auto switches below 768 px |
fullHeight | boolean | Fill 100dvh instead of the parent |
hotkeys, sidebarHotkey | Default on, mod+b | |
edgeSwipe | boolean | Swipe in from the edge to open the drawer |
container | HTMLElement | Portal target for the drawer and sheet |
skipLabel | string | Default Skip to chat |
Keyboard
| Key | Action |
|---|---|
Cmd/Ctrl+B | Collapse or expand the sidebar, open or close the drawer |
Esc | Close the drawer or the panel sheet |
Tab from the top | Skip to chat, which focuses the composer |
←, → on the divider | Resize the panel. Enter collapses |
Accessibility
- Landmarks: the sidebar is an
asidewith anav, the thread ismain, the panel is anasidenamed bypanelLabel. - The drawer and the sheet are modal dialogs that trap focus and return it to the toggle.
- Cmd/Ctrl+B does nothing inside rich text editors, where it means bold.
- Safe-area insets pad the shell, the drawer and the sheet on phones with notches and home indicators.
Motion
- The drawer slides in from the inline start over 500 ms on the drawer curve and leaves in 375 ms, with the scrim on the same curve. Dragging tracks the finger and closes past a third of its width or on a flick.
- The desktop panel enters with 16 px of travel and opacity over 280 ms and leaves in 200 ms; the thread reflows beside it.
- The mobile panel is pushed in from the inline end over 500 ms.
- Anything toggled from the keyboard is instant. Reduced motion keeps short fades and drops the travel.