Skip to content
ShellChat layoutMenu

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.

A full product shell on AI SDK useChat with a scripted coding agent.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/chat-layout.json

Usage

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

An artifact beside the thread on the shared Resizable, closable and draggable.

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.

Phone width: the sidebar as a drawer you can drag closed.

Props

PropTypeNotes
childrenReactNodeThe thread: header, conversation, composer
sidebarReactNodeUsually a ChatSidebar
sidebarCollapsed, defaultSidebarCollapsed, onSidebarCollapsedChangeDesktop rail
sidebarOpen, defaultSidebarOpen, onSidebarOpenChangeMobile drawer
panel, panelOpen, onPanelOpenChangeSide panel, sheet on mobile
panelLabelstringLandmark name and sheet title. Default Panel
panelDefaultSize, panelMinSize, panelMaxSizeResizableSizeDefault 45, 320 px, 70
threadMinSizeResizableSizeDefault 360 px
autoSaveIdstringPersist the split
layout"auto" | "desktop" | "mobile"auto switches below 768 px
fullHeightbooleanFill 100dvh instead of the parent
hotkeys, sidebarHotkeyDefault on, mod+b
edgeSwipebooleanSwipe in from the edge to open the drawer
containerHTMLElementPortal target for the drawer and sheet
skipLabelstringDefault Skip to chat

Keyboard

KeyAction
Cmd/Ctrl+BCollapse or expand the sidebar, open or close the drawer
EscClose the drawer or the panel sheet
Tab from the topSkip to chat, which focuses the composer
←, → on the dividerResize the panel. Enter collapses

Accessibility

  • Landmarks: the sidebar is an aside with a nav, the thread is main, the panel is an aside named by panelLabel.
  • 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.