Skip to content

Message

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.

AIv0.1.0experimentalWCAG 2.2 AAView spec

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.

How do refunds work?

Refunds are issued within 5 business days 1. You can ask for one:

  • from your order page
  • by replying to the receipt

Sources

  1. 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 message

Copies 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.json

Works, 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.

Revenue by week, FebruaryRevenue rose each week, from $9.2K to $15.8K.
Revenue by week4 rows, 3 columns
WeekOrdersRevenue ($)
Wk 13109,200
Wk 238511,400
Wk 344013,100
Wk 451215,800
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

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.

PropTypeDefault
messagerequired

The message, as a list of typed parts.

ChatMessagenone
onApprove

Called with a tool's id when the person approves it.

(toolId: string) => voidnone
onDeny

Called with a tool's id when the person declines it.

(toolId: string) => voidnone
onRetry

Shows a Try again button on a message that failed.

() => voidnone
actions

Extra buttons under the message, e.g. thumbs up and down.

nodenone
focusApproval

Move focus to an approval box when one appears.

booleanfalse
headingLevel

Level of headings inside the reply, 2 to 6.

number3

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