Conversation
Holds a thread that follows new replies.
Row
userdefault
assistant
thinking
retry
error
divider
diff
States
idle
streaming
stopped
error
Usage
When to use it
- Use
- An agent streams replies into a thread and the view should follow them until the person scrolls away.
- Use
- A thread mixes turns, a thinking row, retries, errors and dividers.
- Not for
- A static transcript that never grows. Use Message.
- Not for
- A log of lines with levels and search. Use Code block.
Rules
6 rules
- Must
- Set status to streaming while a turn runs and to idle, stopped or error when it ends. Because it drives aria-busy, the height floor that keeps the view from jumping, the thinking row and the one announcement per turn end.
- Must
- Call snap() from useConversation when the person sends; wrap the composer in ConversationProvider to reach it. Because the view lets go only on a gesture, so a person reading earlier turns would send and not see their own message.
- Never
- Scroll the viewport yourself on every delta. Because it fights the person who scrolled up to read; the viewport follows while attached and lets go only on a wheel, swipe, key or scrollbar drag up.
- Should
- Pass rows to ConversationTimeline as typed data and keep each row object stable. Because structurally equal rows keep their identity, so a delta re-renders only the row it touched.
- Must
- Show a stopped turn as a divider row with reason "interrupted", never as an error row. Because the person stopped it on purpose, and an error says something broke.
- Never
- Put the thread in a card or a panel; it is a scroll column on the page. Because the person's bubbles are the only boxes in a thread, and a shell around it adds a frame to everything inside.
Accessibility
4 checks
- The viewport is role="log" with aria-live off and one status region that says complete, stopped or failed when a turn ends.
- The viewport is focusable with the inset ink ring; ArrowUp, PageUp and Home let go of the end, End returns.
- Jump to latest is inert while near the end; when it hides with focus, focus moves to the thread.
- Waiting on you goes to the waiting call and focuses its first action, Deny.
Keyboard
2 keys
- ArrowUp, PageUp, Home
- Scroll up and stop following.
- End
- Returns to the end and follows again.
Edge cases
5 edge cases
- Selecting text while it streams
- Following pauses until the selection collapses.
- A loader replaced by shorter text
- The height floor holds while streaming, so the view never jumps.
- History prepended above
- The row under the reader stays where it was.
- Long thread
- Offscreen rows skip layout; the last user row and the last row are always laid out.
- Reduced motion
- Jumps scroll at once instead of smoothly.
API
8 props
status'idle' | 'streaming' | 'stopped' | 'error'default'idle'ConversationViewport aria-label, labelsstring, {complete, stopped, failed}default"Conversation"ConversationTimeline rowsConversationRow[]defaultrequiredConversationTimeline renderRow, onRetry, labelsfunctions, ConversationLabelsConversationJump waitingbooleandefaulttrueConversationItem rowKey, pinnedstring, booleanuseConversation(){attached, distance, atEnd, scrollToEnd, snap, detach}useStickToBottom(ref)hook
Tokens
9 tokens
--page- #fbfbfa
--fail-tint- #fbe9e5
--fail-text- #b23c29
--radius-overlay- 10px
--control- 32px
--row- 36px
--z-raised- 10
--focus- rgb(5 5 5 / 0.80)
--dur-state- 160ms