One message in a conversation, from you or the assistant. It renders each part in order with the right component: text as formatted markdown, tool calls as activity, tables and charts as real components, sources at the end.
Bring your own model.
These components only show what you give them, in one message shape, and report what the person does. They never call a model or a server, so they work with any backend. They are built for how assistive technology handles streaming text, tool steps and approvals.
Refunds are issued within 5 business days 1. You can ask for one:
- from your order page
- by replying to the receipt
Sources
- Refund policy (opens in a new tab)example.comRefunds are issued within 5 business days.
import { Message, type ChatMessage } from "@rdloom/react";
const question: ChatMessage = { id: "1", role: "user", parts: [{ type: "text", text: "How do refunds work?" }] };
const answer: ChatMessage = {
id: "2",
role: "assistant",
parts: [
{ type: "text", text: "Refunds are issued within **5 business days** [1]. You can ask for one:\n\n- from your order page\n- by replying to the receipt" },
{ type: "citation", id: "policy", title: "Refund policy", url: "https://example.com/policy", snippet: "Refunds are issued within 5 business days." },
],
};
export default function MessageTextExample() {
return (
<div className="flex w-[36rem] max-w-full flex-col gap-6">
<Message message={question} />
<Message message={answer} />
</div>
);
}Installation
npx rdloom add messageCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/message.jsonWorks, but without upgrade tracking.
Usage
import { Message, type ChatMessage } from "@rdloom/react";
<div className="flex w-[36rem] max-w-full flex-col gap-6">
<Message message={question} />
<Message message={answer} />
</div>With tools
Orders peaked in the third week.
import { Message, type ChatMessage } from "@rdloom/react";
const message: ChatMessage = {
id: "1",
role: "assistant",
parts: [
{ type: "reasoning", text: "The question is about last month, so I'll query orders for February and group them by week." },
{ type: "tool", id: "q", name: "query_orders", title: "Searching your orders", state: "done", startedAt: "2026-03-01T10:00:00Z", endedAt: "2026-03-01T10:00:02Z", input: { month: "2026-02" }, output: { rows: 4 } },
{ type: "tool", id: "c", name: "make_chart", title: "Creating a chart", state: "done" },
{ type: "text", text: "Orders peaked in the third week." },
],
};
export default function MessageWithToolsExample() {
return (
<div className="w-[36rem] max-w-full">
<Message message={message} />
</div>
);
}With artifacts
Here is last month's revenue by week, and the same numbers as a table.
import { Message, type ChatMessage } from "@rdloom/react";
const message: ChatMessage = {
id: "1",
role: "assistant",
parts: [
{ type: "text", text: "Here is last month's revenue by week, and the same numbers as a table." },
{
type: "artifact",
kind: "chart",
title: "Revenue by week, February",
summary: "Revenue rose each week, from $9.2K to $15.8K.",
data: { labels: ["Wk 1", "Wk 2", "Wk 3", "Wk 4"], series: [{ name: "Revenue", values: [9200, 11400, 13100, 15800] }], unit: "$" },
},
{
type: "artifact",
kind: "table",
title: "Revenue by week",
data: {
columns: ["Week", "Orders", "Revenue ($)"],
rows: [["Wk 1", 310, 9200], ["Wk 2", 385, 11400], ["Wk 3", 440, 13100], ["Wk 4", 512, 15800]],
},
},
],
};
export default function MessageWithArtifactsExample() {
return (
<div className="w-[40rem] max-w-full">
<Message message={message} />
</div>
);
}Error
I started looking, but
Something went wrong.
Here is the first part of the answer
Stopped
import { Message, type ChatMessage } from "@rdloom/react";
const failed: ChatMessage = { id: "1", role: "assistant", status: "error", parts: [{ type: "text", text: "I started looking, but" }] };
const stopped: ChatMessage = { id: "2", role: "assistant", status: "stopped", parts: [{ type: "text", text: "Here is the first part of the answer" }] };
export default function MessageErrorExample() {
return (
<div className="flex w-[36rem] max-w-full flex-col gap-6">
<Message message={failed} onRetry={() => {}} />
<Message message={stopped} />
</div>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
messagerequiredThe message, as a list of typed parts. | ChatMessage | none |
onApproveCalled with a tool's id when the person approves it. | (toolId: string) => void | none |
onDenyCalled with a tool's id when the person declines it. | (toolId: string) => void | none |
onRetryShows a Try again button on a message that failed. | () => void | none |
actionsExtra buttons under the message, e.g. thumbs up and down. | node | none |
focusApprovalMove focus to an approval box when one appears. | boolean | false |
headingLevelLevel of headings inside the reply, 2 to 6. | number | 3 |
Accessibility
Role article, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Tab: moves through links, code copy buttons, tool steps and message actions
Screen readers announce
- "Assistant message, article" then the reply
- "Assistant is thinking" before any text arrives
- "Something went wrong. Try again"
What your code must do
- Each message is an article named by who wrote it ("You message", "Assistant message")
- Your own text is plain text; the assistant's is formatted, with no HTML injected
- A reply that hasn't started shows "Assistant is thinking" for screen readers as well as animated dots, and the dots stand still for reduced motion
- Errors are announced (role alert) and offer Try again when you provide onRetry; a stopped reply says so
- aria-busy is set while the message streams
- A picture the person attached is shown small, with its file name as alt text, only when it comes from this device (blob: or data: address); other files show as a chip
Guidelines
Use it when
- Showing one turn of a chat
- Showing a saved conversation
Avoid it when
- A whole chat with input and scrolling: use Chat
Don't
- Building the message from one long markdown string, so tools, charts and sources lose their structure
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-text-default--rd-color-text-muted--rd-color-surface-selected--rd-color-surface-subtle--rd-color-border-default--rd-color-action-primary--rd-color-feedback-danger--rd-color-focus-ring--rd-radius-overlay--rd-radius-control