Shows an assistant's reply as formatted text: headings, lists, tables, code, links and source markers. It is safe for text you do not control and for text that is still arriving.
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.
Quarterly summary
Revenue grew 12% over last quarter, driven by new subscriptions. The three biggest changes:
- Annual plans rose to
38%of sign-ups - Churn fell below 2%
- Support tickets per customer dropped
| Region | Revenue | Change |
|---|---|---|
| Europe | $412K | +9% |
| Americas | $655K | +14% |
Numbers are preliminary until the books close.
const growth = (now: number, before: number) => (now - before) / before;See the full report (opens in a new tab) for details.
import { Response } from "@rdloom/react";
const reply = `## Quarterly summary
Revenue grew **12%** over last quarter, driven by _new subscriptions_. The three biggest changes:
1. Annual plans rose to \`38%\` of sign-ups
2. Churn fell below 2%
3. Support tickets per customer dropped
| Region | Revenue | Change |
| :----- | ------: | -----: |
| Europe | $412K | +9% |
| Americas | $655K | +14% |
> Numbers are preliminary until the books close.
\`\`\`ts
const growth = (now: number, before: number) => (now - before) / before;
\`\`\`
See the [full report](https://example.com/report) for details.`;
export default function ResponseMarkdownExample() {
return (
<div className="w-[34rem] max-w-full">
<Response>{reply}</Response>
</div>
);
}Installation
npx rdloom add responseCopies 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/response.jsonWorks, but without upgrade tracking.
Usage
import { Response } from "@rdloom/react";
<div className="w-[34rem] max-w-full">
<Response>{reply}</Response>
</div>Streaming
import { useEffect, useState } from "react";
import { Button, Response } from "@rdloom/react";
const reply = "Checking the numbers now. **Sales rose in March**, and the biggest gain came from the new annual plan:\n\n- Annual plans: up 18%\n- Monthly plans: flat\n\n```sql\nSELECT month, SUM(total) FROM orders GROUP BY month;\n```";
// The text arrives a few characters at a time. Pass isStreaming while it does: the caret shows, and
// a screen reader waits for the finished text instead of reading every fragment.
export default function ResponseStreamingExample() {
const [shown, setShown] = useState(0);
const [run, setRun] = useState(0);
const done = shown >= reply.length;
useEffect(() => {
setShown(0);
const id = setInterval(() => setShown((n) => (n >= reply.length ? n : n + 3)), 40);
return () => clearInterval(id);
}, [run]);
return (
<div className="flex w-[34rem] max-w-full flex-col items-center gap-3">
<Response className="w-full" isStreaming={!done}>{reply.slice(0, shown)}</Response>
<Button variant="secondary" size="sm" onPress={() => setRun((n) => n + 1)}>
Replay
</Button>
</div>
);
}With citations
Sources
- Operations handbook (opens in a new tab)example.comRefunds are issued within 5 business days.
- Refund policy (opens in a new tab)example.com
import { Response, Sources, type CitationPart } from "@rdloom/react";
const sources: CitationPart[] = [
{ type: "citation", id: "ops", title: "Operations handbook", url: "https://example.com/handbook", snippet: "Refunds are issued within 5 business days." },
{ type: "citation", id: "policy", title: "Refund policy", url: "https://example.com/policy" },
];
// [1] and [2] in the text become markers that jump to the matching entry in Sources.
export default function ResponseWithCitationsExample() {
return (
<div className="flex w-[34rem] max-w-full flex-col gap-4">
<Response citations={sources}>
Refunds go out within 5 business days [1], and only for orders placed in the last 30 days [2].
</Response>
<Sources sources={sources} />
</div>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
childrenrequiredThe reply, as markdown. | string | none |
isStreamingTrue while the reply is still arriving. Shows a caret and tells assistive technology the content is changing. | boolean | false |
citationsSources for the [1], [2] markers in the text. A marker with no matching source stays as plain text. | CitationPart[] | none |
headingLevelLevel of the reply's first heading (a # line), 2 to 6. Replies sit inside a page, so it starts at 3. | number | 3 |
Accessibility
Role none, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Tab: moves to code blocks (copy button), long code that scrolls, wide tables and links
Screen readers announce
- Headings, lists and tables are announced with their real structure
- A copy button reads as, e.g. "Copy typescript code", then "Copied to clipboard"
- Source markers read as "Source 2: <title>"
What your code must do
- No raw HTML is ever injected: model output can't add markup or scripts
- Links with unsafe schemes (javascript:, data:) show as plain text; links that open a new tab say so in words
- Bare web addresses become links without taking the punctuation after them; a backslash keeps a character literal
- Images in the text are never loaded (an address chosen by a model could track the reader): they show as a link named "<alt text> (image)"
- Code blocks and tables that scroll sideways can be focused and scrolled with the keyboard
- The copy button's name includes the language, and it confirms with a status message
- While streaming, the region has aria-busy so a screen reader doesn't read each fragment
- Half-finished markdown (an open code block, a cut-off list) never throws and still reads sensibly
Guidelines
Use it when
- Showing what an AI assistant wrote
- Any markdown from a source you can't fully trust
Avoid it when
- Authoring or editing markdown: this only displays it
- Showing a user's own message: use plain text
Don't
- Rendering model output with dangerouslySetInnerHTML
- Re-mounting the component on every streamed chunk instead of updating its text
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-subtle--rd-color-border-default--rd-color-border-strong--rd-color-action-primary--rd-color-focus-ring--rd-radius-control