Chat
ProThe AI conversation thread, the second piece of the AI module. The assistant answers as a document, the Notion and ChatGPT style; the user turn stays a bubble. Streaming and reasoning included.
Installation#
Usage#
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
Anatomy#
Every part is a named export, never dot-notation, so each one crosses the server and client boundary cleanly. Conversation is the scrollable thread; each Message carries its role to its parts through context, so one prop drives the side, tint, and meta alignment. Compose only the parts a turn needs.
import {
Conversation,
ConversationScrollButton,
Message,
MessageAvatar,
MessageBody,
MessageHeader,
MessageName,
MessageTime,
MessageReasoning,
MessageReasoningTrigger,
MessageReasoningContent,
MessageReasoningStep,
MessageReasoningFiles,
MessageReasoningFile,
MessageReasoningFilesMore,
MessageReasoningQueries,
MessageReasoningQuery,
MessageReasoningSources,
MessageReasoningSource,
MessageContent,
MessageTyping,
MessageActions,
MessageAction,
} from "@/components/ui/chat"
<Conversation>
<Message>
<MessageAvatar />
<MessageBody>
<MessageHeader>
<MessageName />
<MessageTime />
</MessageHeader>
<MessageReasoning>
<MessageReasoningTrigger />
<MessageReasoningContent>
<MessageReasoningStep>
<MessageReasoningQueries>
<MessageReasoningQuery />
</MessageReasoningQueries>
<MessageReasoningSources>
<MessageReasoningSource />
</MessageReasoningSources>
<MessageReasoningFiles>
<MessageReasoningFile />
<MessageReasoningFilesMore />
</MessageReasoningFiles>
</MessageReasoningStep>
</MessageReasoningContent>
</MessageReasoning>
<MessageContent>
<MessageTyping />
</MessageContent>
<MessageActions>
<MessageAction />
</MessageActions>
</MessageBody>
</Message>
<ConversationScrollButton />
</Conversation>Roles#
A message's role is the only switch. assistant aligns left with an avatar and a muted bubble; user aligns right with a solid blue bubble. Both use a soft, uniform radius on all four corners.
With avatar (bubble)#
The default is the document style above. For a chat with an identity, the “robotized” look, set variant="bubble" on the Conversation: every turn gets a bubble, and you pair the assistant with a MessageAvatar, name, time, and actions. A per-message variant overrides the conversation default.
Rich content#
MessageContent is markup-agnostic. Drop in plain text or your own rendered markdown; the bubble styles the common elements (paragraphs, headings, lists, inline code, pre blocks, and links) through descendant selectors, tinted to read on either bubble.
Actions#
MessageActions holds a row of MessageAction buttons for copy, regenerate, and feedback. Each wraps our Button, so it inherits the focus ring and a tooltip drawn from its aria-label.
Typing indicator#
While a response streams, drop a MessageTyping inside a MessageContent. Three dots ripple left to right, and hold still under reduced-motion.
Reasoning#
The collapsible “thinking” block, the way Claude and ChatGPT show it. While streaming the icon pulses and the trigger reads “Investigating…”; once done, it settles to a summary like “Investigated 14 sources” that anyone can expand. For a simple answer, pass plain text; for an agentic run, the block becomes a log of what it did. Group each phase in a MessageReasoningStep (an optional label captions it, a dot pins onto the rail so a column reads as a timeline, and status="active" pulses the in-progress one). Show the searches it ran with MessageReasoningQuery and the URLs it consulted with MessageReasoningSource, as compact favicon and domain chips by default or a bordered title list via layout="list". The domain and favicon are derived from the href. Built on Radix Collapsible, so keyboard and ARIA come for free. Press Ask again to watch it stream through the phases.
Agent trace#
For a coding agent, write each step as one line with title, in the present while it runs (“Reading 3 of 6 files”) and in the past once done (“Read 6 files about sessions and cookies”). The live line takes full ink and a slow sweep; finished lines go muted. Give a step an icon for its kind, a search or the final check. Mark an approach that went nowhere with status="dead-end": a cross on the rail, and the line fades back. Set elapsed on the trigger and it counts “Thinking 9s”, then settles to “Thought for 14s”. Press Run again to watch the full trace.
Jump to latest#
Drop a ConversationScrollButton as the last child of a Conversation. It stays hidden while the reader is at the newest turn; scroll up and a pill fades in, centered over the thread's bottom edge (a sticky row, so it never scrolls with the history and takes no room in it). Pressing it glides back down and resumes following. The follow survives streaming and resizing: a turn that grows by itself, or a panel that changes size, keeps a reader who was at the bottom there, and the thread's own glide never flashes the pill on the way down. Stream the answer below, then scroll up while it writes.
The Conversation must be the element that scrolls: give it a height (here min-h-0 flex-1 in a sized column; in an AI Panel, <AIPanelBody scrollable={false}>).
API reference#
Conversation
The scrollable thread, a role="log" polite live region; give it a height or max-height to scroll. Forwards all native <div> props. Every part accepts className, merged last.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "plain" | "bubble" | "plain" | The style of every turn: the assistant answer as a page, or every turn a bubble. |
| stickToBottom | boolean | true | Follows new turns while the reader is already at the bottom; scrolling up is never yanked back. The first pass lands instantly, later ones glide. Turn it off for a static transcript that opens at the top. |
| Data attribute | Description |
|---|---|
| data-variant | "plain" or "bubble". |
ConversationScrollButton
The "jump to latest" pill: place it as the last child of a Conversation. It shows only while the reader has scrolled away from the newest turn, floats centered over the thread's bottom edge, and on press glides back down and resumes following. The Conversation must be the element that scrolls (min-h-0 flex-1, or AIPanelBody scrollable={false}). Takes every Button prop except asChild.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | "Jump to latest" | The pill's text, also its accessible name. |
| children | React.ReactNode | - | Replaces the default arrow and label. |
| variant | ButtonProps["variant"] | "outline" | Any Button variant. |
| size | "sm" | "md" | "lg" | "xl" | "sm" | The Button size. |
| Data attribute | Description |
|---|---|
| data-state | "visible" or "hidden", on the sticky row that wraps the button. |
Message
One turn; its role flows to every part. Forwards all native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| role | "assistant" | "user" | "assistant" | Who sent it: drives the side, the alignment and the bubble tint. |
| variant | "plain" | "bubble" | - | Overrides the style of the conversation for this turn. |
| Data attribute | Description |
|---|---|
| data-role | "assistant" or "user". |
MessageAvatar
The sender avatar, an Avatar that takes every Avatar root prop. Children replace the initials (with an icon, say).
| Prop | Type | Default | Description |
|---|---|---|---|
| src | string | - | The image URL. |
| name | string | - | The alt text, and the source of the initials fallback. |
| size | "xs" | "sm" | "md" | "lg" | "xl" | "sm" | The Avatar size. |
MessageBody
The column holding header, content and actions, aligned to the message side. Forwards all native <div> props.
MessageHeader
The optional meta row above the content. Forwards all native <div> props.
MessageName
The sender name. Forwards all native <span> props.
MessageTime
The muted timestamp. Forwards all native <span> props.
MessageContent
The bubble (or, for a plain assistant turn, the page), markup-agnostic. Forwards all native <div> props.
MessageActions
The row of actions under a turn. Forwards all native <div> props.
MessageAction
One action (copy, regenerate): a Button that takes every Button prop; its aria-label names it and feeds the tooltip.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | ButtonProps["variant"] | "ghost" | Any Button variant. |
| size | "sm" | "md" | "lg" | "xl" | "sm" | The Button size. |
| iconOnly | boolean | true | A square icon button. |
MessageTyping
Three animated dots, a role="status" labelled "Assistant is typing"; put it in a MessageContent. Forwards all native <div> props.
MessageReasoning
The collapsible thinking block, over Radix Collapsible.Root: forwards all its props.
| Prop | Type | Default | Description |
|---|---|---|---|
| streaming | boolean | false | The model is still thinking: the icon pulses and the default label reads "Thinking…". |
| open | boolean | - | Open state, controlled. |
| defaultOpen | boolean | false | Open state on first render, uncontrolled. |
| onOpenChange | (open: boolean) => void | - | Called when it opens or closes. |
| Data attribute | Description |
|---|---|
| data-state | "open" or "closed", from Radix. |
MessageReasoningTrigger
The one-line toggle, over Radix Collapsible.Trigger. Children are the label ("Thought for 6s"); without them it rolls between "Thinking…" and "Reasoning".
| Prop | Type | Default | Description |
|---|---|---|---|
| elapsed | boolean | number | - | true keeps its own clock: "Thinking 9s" while streaming, then "Thought for 14s". A number shows that many seconds. Ignored when you pass children. |
| Data attribute | Description |
|---|---|
| data-state | "open" or "closed". Turns the caret. |
MessageReasoningContent
The chain of thought behind a left rail, over Radix Collapsible.Content. className lands on the inner <div>.
| Data attribute | Description |
|---|---|
| data-state | "open" or "closed". |
MessageReasoningStep
One phase of the reasoning log, with a node on the rail. Forwards native <div> props except title.
| Prop | Type | Default | Description |
|---|---|---|---|
| title | React.ReactNode | - | The one line an agent-trace step reads as. Full ink and a sweep while active, muted once done, fainter as a dead end. |
| label | React.ReactNode | - | A quiet caption above the step ("Searching"). |
| status | "active" | "done" | "dead-end" | "done" | active turns the dot into an ink pulse; dead-end swaps it for a cross and fades the title. |
| marker | boolean | true | Draws the rail node (the dot, or the icon). |
| icon | React.ReactNode | - | A glyph in place of the dot (14px unless it carries its own size-*), such as a search or the final check. |
| Data attribute | Description |
|---|---|
| data-status | "active", "done" or "dead-end". |
MessageReasoningFiles
A wrapping row of file pills. Forwards all native <div> props.
MessageReasoningFile
One file or page the model opened: a pill, not a link. Forwards all native <span> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| name* | string | - | The file name; its extension picks the glyph (ts, tsx, css, md…). Children replace it as the label. |
| icon | React.ReactNode | - | Overrides the derived glyph. |
MessageReasoningFilesMore
The "+3 more" that closes a row of files. Forwards all native <span> props.
MessageReasoningQueries
A wrapping row of query pills. Forwards all native <div> props.
MessageReasoningQuery
One search the model ran: a monospace pill with a magnifier. Forwards all native <span> props.
MessageReasoningSources
The URLs the model consulted. Forwards all native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| layout | "chips" | "list" | "chips" | Compact domain pills, or a bordered list of titled rows. Flows to each source. |
| Data attribute | Description |
|---|---|
| data-layout | "chips" or "list". |
MessageReasoningSource
One source, an <a> that opens in a new tab. Forwards native <a> props except title.
| Prop | Type | Default | Description |
|---|---|---|---|
| href* | string | - | The URL; the domain and favicon derive from it. |
| title | React.ReactNode | - | The headline in the list layout; the domain by default. |
| favicon | string | - | Overrides the derived favicon; a globe shows if it fails. |