When to use it
User selects a conversation. Right pane on desktop, full screen on mobile.
- Smooth scroll-to-bottom on new message
- Edit own messages within 5 minutes
- Reactions in 2 taps
Layout regions
- header
- messages
- composer
States
- default
- loading
- empty
- partial
Example
Next.js — Server Component shell + client-side WS thread
// app/chat/[id]/page.tsx — Server Component
import { fetchThread } from '@/lib/api.server'
import { ChatThreadClient } from './chat-thread-client'
export default async function ChatPage({ params }) {
const initial = await fetchThread((await params).id)
return <ChatThreadClient threadId={initial.id} initial={initial} />
}
// chat-thread-client.tsx
'use client'
import { useEffect, useRef, useState } from 'react'
import useWebSocket from 'react-use-websocket'
export function ChatThreadClient({ threadId, initial }) {
const [messages, setMessages] = useState(initial.messages)
const scrollRef = useRef<HTMLDivElement>(null)
const { sendJsonMessage } = useWebSocket('wss://api.brand.com/chat', {
onMessage: (e) => setMessages((m) => [...m, JSON.parse(e.data)]),
shouldReconnect: () => true,
})
useEffect(() => {
const el = scrollRef.current
if (el && el.scrollTop + el.clientHeight > el.scrollHeight - 200) {
el.scrollTop = el.scrollHeight
}
}, [messages.length])
return (
<Stack className="h-full">
<ChatHeader />
<ScrollArea ref={scrollRef} className="flex-1" role="log" aria-live="polite">
<Stack size="2">{messages.map((m) => <Bubble key={m.id} {...m} />)}</Stack>
</ScrollArea>
<Composer onSubmit={(text) => sendJsonMessage({ threadId, text })} />
</Stack>
)
}The full Chat — thread pattern specifies 3 exact microcopy strings, 2 motion specs, 5 composition rules, 2 more code examples — all gated behind the full recipe. Get the full recipe.