Response

Streaming-safe markdown for assistant replies — Streamdown themed to Reva, graceful with unterminated syntax mid-stream.

const [text, setText] = useState("");// append streamed chunks to state, then:<Response>{text}</Response>

Usage

import { Response, ResponseLink } from "@reva/ui";

<Response>{message.text}</Response>;

Response renders an assistant reply's markdown — GFM included (tables, task lists, strikethrough, footnotes) — through Streamdown, with every element re-themed onto Reva primitives and tokens: paragraphs are Text, headings are Heading on a chat-scale ramp, tables echo the house Table look, and fenced code highlights on the same lazy shiki singleton as Code Block.

It is streaming-safe by default: unterminated **bold, `code`, links, and code fences mid-stream are auto-completed by Streamdown's incomplete-markdown parser (parseIncompleteMarkdown, on by default). Don't reimplement any of that — just keep appending to the string.

Response is purely presentational — no fetch or stream logic. It is also memoized the AI Elements way: it re-renders only when children (the markdown string) or isAnimating changes, so streaming appends stay cheap. Other prop changes after mount are ignored by design — pass a key to force a remount if a playground or demo needs to toggle props live.

Examples

Static GFM

The same renderer with a settled string — table, list, and a fenced code block.

Two portfolios are outside tolerance this week:

  • Hargreaves ISA equities at 68% against a 60% target
  • Patel SIPP bonds 4.2% under target
PortfolioTargetActualDrift
Hargreaves ISA60%68%+8.0%
Patel SIPP35%30.8%−4.2%
ts
const flagged = portfolios.filter(  (portfolio) => Math.abs(portfolio.drift) > TOLERANCE,);
<Response>{`Two portfolios are outside tolerance this week:- **Hargreaves ISA** — equities at 68% against a 60% target- **Patel SIPP** — bonds 4.2% under target| Portfolio | Target | Actual | Drift || --- | --- | --- | --- || Hargreaves ISA | 60% | 68% | +8.0% || Patel SIPP | 35% | 30.8% | −4.2% |```tsconst flagged = portfolios.filter((portfolio) => Math.abs(portfolio.drift) > TOLERANCE,);````}</Response>

Overriding element renderers

Pass components to replace any element renderer — entries merge over the Reva map. Two contracts to respect:

  • Referential stability. Streamdown's per-block memoization compares components by identity — pass a module constant (or memoized object), never an inline literal, or every streamed token re-renders every block.
  • Remount to change. Because of the memo above, swapping components after mount has no effect on its own — change the key too.
import { Response, ResponseLink, type ResponseLinkProps } from "@reva/ui";

// Module-level: referentially stable across renders.
const COMPONENTS = {
  a: (props: ResponseLinkProps) =>
    isCitationHref(props.href) ? <CitationMarker {...props} /> : <ResponseLink {...props} />,
};

<Response components={COMPONENTS}>{text}</Response>;

ResponseLink is exported for exactly this: a custom a override can handle its special case (the citation pattern above — detailed on the Sources page) and fall back to the default link behaviour for everything else — hardened external links (target="_blank" rel="noopener noreferrer" pass through from Streamdown's pipeline), in-page anchors navigating in place, and still-streaming links rendering inert until their URL completes.

Highlighting and plugins

Code fences highlight through the response code plugin on the shared lazy shiki singleton — nothing shiki-related loads until a fence actually renders, and plain code shows until the chunk resolves. Consumer plugins merge over the default:

// Opt out of fence highlighting entirely:
<Response plugins={{ code: undefined }}>{text}</Response>

Note the shikiTheme prop is inert here by design — the plugin owns the light/dark theme pair, and its themes take precedence.

Props

PropTypeDefaultDescription
childrenstringThe markdown source — typically the streamed assistant message text. Required.
parseIncompleteMarkdownbooleantrueAuto-complete unterminated syntax mid-stream.
componentsStreamdownComponentsReva renderer mapPer-element overrides, merged over the Reva map. Must be referentially stable; pair changes with a key remount.
pluginsStreamdown pluginsresponse code pluginMerged over the default; { code: undefined } disables fence highlighting.
isAnimatingbooleanStreaming signal forwarded to Streamdown; one of the two props the memo watches.
classNamestringApplied to the wrapping data-slot="response" element.

Remaining StreamdownProps pass through, with two exceptions: shikiTheme is inert (the code plugin owns the theme pair), and linkSafety is omitted from the type — only Streamdown's default a renderer consults it, and Response always replaces a with ResponseLink.

ResponseLink accepts the intrinsic a props plus Streamdown's node (consumed, never forwarded to the DOM) — see ResponseLinkProps.

On this page