Chat Message
A message row with streamed markdown, reasoning, tool calls, attachments, inline edit and hover actions.
Built on Code Block, Copy Button, Icons, Reasoning and Tool Call.
Installation
npx shadcn@latest add https://neonkit.heyaadi.com/r/chat-message.jsonUsage
import { ChatMessage, ChatReasoning, ChatToolCall } from "@/components/ui/chat-message"
<ChatMessage from="assistant" name="Argon" time="14:02" content={text} streaming={streaming}>
<ChatReasoning streaming={thinking}>{reasoning}</ChatReasoning>
<ChatToolCall name="read_file" args={{ path: "app/page.tsx" }} status="done" />
</ChatMessage>Examples
Bubble layout
Your own messages sit on the right in a bubble with their images above; replies stay open on the left.
Tool calls
Done, failed, running and queued. Rows open to their arguments and output. These are Tool Call rows; use ToolCallGroup to join them into one log.
Reasoning
The filament is hot while it thinks and the trace stays open. Open or close it by hand and it stops folding itself away.
System notes and errors
System messages center as a quiet note. An error keeps whatever arrived and offers Try again.
API reference
ChatMessage
Renders an article. Children (reasoning, tool calls, your own parts) come before content.
| Prop | Type | Default |
|---|---|---|
from | "user"Who sent it. System messages render as a centered note. | None |
name | ReactNodeShown above flat messages and used as the accessible name. | None |
avatar | ReactNodeReplaces the default mark. null hides it. | None |
time | ReactNodeShown next to the name. | None |
dateTime | stringMachine-readable value for the time element. | None |
layout | "flat"Rows with marks and names, or user bubbles on the right. | "flat" |
content | stringMarkdown-ish text: paragraphs, lists, quotes, headings, fenced code, inline code, bold, italic, links. | None |
streaming | booleanCaret, fresh-word fade, lit mark. Actions hide until it's done. | false |
attachments | ChatAttachmentItem[]{ name, size?, type?, url? }. Images with a url show a thumbnail. | None |
error | ReactNodeShown under the content with Try again when onRetry is set. | None |
actions | "hover"When the action row shows. Touch screens always show it. | "hover" |
copyText | stringWhat the copy action copies. | content |
onRetry | () => voidAdds Retry to replies. | None |
onEdit | () => voidAdds Edit to your own messages. | None |
feedback | "up"Controlled thumbs. Shown on replies when any feedback prop is set. | None |
defaultFeedback | "up"Initial thumbs when uncontrolled. | None |
onFeedbackChange | (feedback) => voidPressing the active thumb again clears it to null. | None |
extraActions | ReactNodeMore buttons at the end of the action row. | None |
color | NeonColorPalette name or hex. Falls back to --neon-accent, then cyan. | inherited |
ChatReasoning
Reasoning with followStreaming on: it opens while thinking and folds away when done. Takes every Reasoning prop.
| Prop | Type | Default |
|---|---|---|
streaming | booleanHot filament, running timer, open. | false |
duration | numberSeconds spent. Measured while streaming when omitted. | None |
open / defaultOpen / onOpenChange | boolean / boolean / (open) => voidDisclosure state. defaultOpen follows streaming. | None |
followStreaming | booleanSet false for a trace that stays folded to its one live line. | true |
label | ReactNodeHeader while streaming. | "Thinking" |
ChatToolCall
ToolCall with its arguments always summarised as key: value pairs. Takes every ToolCall prop, including approval.
| Prop | Type | Default |
|---|---|---|
name | stringThe tool's name. | None |
args | Record<string, unknown>Summarised in the row, listed when open. | None |
status | "pending"Drives the lamp, the readout and the border. | "done" |
result | ReactNodeShown when open. Strings render as code. | None |
error | ReactNodeShown under the row. | None |
duration | numberSeconds. A live timer counts while running. | None |
open / defaultOpen / onOpenChange | boolean / boolean / (open) => voidDisclosure state. | None |
ChatMarkdown
The renderer on its own, for building your own message parts.
| Prop | Type | Default |
|---|---|---|
children | stringThe text so far. | None |
streaming | booleanCaret at the end and fresh-word fade. | false |
codeMaxHeight | numberCode blocks scroll past this height. | 420 |
Accessibility
- Each message is an article named by its sender. A streaming message sets aria-busy so screen readers wait for the finished text.
- Hover-only actions also appear on keyboard focus within the message and are always visible on touch screens.
- Thumbs are toggle buttons with aria-pressed. Copy announces through a polite status region.
- Reasoning and tool rows are disclosure buttons with aria-expanded; closed panels are hidden from assistive tech, not just collapsed.
- Unclosed code fences, backticks and bold render formatted while text streams, so the reply doesn't flash raw markdown.
- Links only render for http(s), mailto and relative URLs. External ones open in a new tab with noopener.
- Under prefers-reduced-motion the caret holds still, words appear without the fade and the reply mark stops breathing.