ChatMessageexperimental
Renders one chat message and its ordered parts: markdown text, reasoning, tool calls, attachments, and a citation footer, with copy and regenerate actions and per-role styling.
Playground
Change the layout, reply role, and density to see how a message renders. The bubble layout tints and aligns by role; plain is a full-width column. Hover a message to reveal
its copy and regenerate bar. The live playground shows a full streaming conversation.
How do I center an element with flexbox?
Make the parent a flex container and center on both axes:
justify-content: centersets the horizontal positionalign-items: centersets the vertical position
The child then sits in the middle of the container.
<script lang="ts">
import { ChatMessage } from '@urbicon-ui/blocks';
const thread = [
{
id: 'pg-question',
role: 'user',
parts: [{ type: 'text', text: 'How do I center an element with flexbox?' }],
createdAt: new Date('2026-01-01T09:40:00.000Z'),
status: 'complete'
},
{
id: 'pg-reply',
role: 'assistant',
parts: [
{
type: 'text',
text: `Make the parent a flex container and center on both axes:
1. \`justify-content: center\` sets the horizontal position
2. \`align-items: center\` sets the vertical position
The child then sits in the **middle** of the container.`
}
],
createdAt: new Date('2026-01-01T09:41:00.000Z'),
status: 'complete'
}
];
function regenerate() {
// re-run the last assistant turn
}
</script>
{#each thread as message (message.id)}
<ChatMessage
{message}
onRegenerate={message.role === 'assistant' ? regenerate : undefined}
/>
{/each}01 Examples
An agentic message
The user wants sources. Look up the attention paper and the scaling-laws work, then cite both inline.
{
"query": "transformer attention scaling laws"
} {
"hits": 2
} The Transformer replaced recurrence with self-attention , and later work showed its performance scales predictably with compute .
<script lang="ts">
import { ChatMessage, type ChatMessageData } from '@urbicon-ui/blocks';
// A single assistant message whose ordered parts exercise the whole dispatch:
// reasoning → tool-call → text, with two sources collected into the footer.
const message: ChatMessageData = {
id: 'agentic-1',
role: 'assistant',
parts: [
{
type: 'reasoning',
text: 'The user wants sources. Look up the attention paper and the scaling-laws work, then cite both inline.',
durationMs: 2400
},
{
type: 'tool-call',
id: 'tc-search',
name: 'search_papers',
state: 'complete',
input: { query: 'transformer attention scaling laws' },
output: { hits: 2 }
},
{
type: 'text',
text: 'The Transformer replaced recurrence with self-attention [1], and later work showed its performance scales predictably with compute [2].'
},
{
type: 'source',
id: '1',
title: 'Attention Is All You Need',
url: 'https://arxiv.org/abs/1706.03762',
snippet: 'We propose a new simple network architecture, the Transformer.'
},
{
type: 'source',
id: '2',
title: 'Scaling Laws for Neural Language Models',
url: 'https://arxiv.org/abs/2001.08361',
snippet: 'Performance improves smoothly with model size, data and compute.'
}
],
createdAt: new Date('2026-01-01T09:41:00'),
status: 'complete'
};
</script>
<div class="w-full max-w-2xl">
<ChatMessage {message} layout="plain" onRegenerate={() => {}} />
</div>
Custom tool-call renderer
It's 7 °C and overcast in Berlin right now.
<script lang="ts">
import { ChatMessage, type ChatMessageData, type ChatToolCallPart } from '@urbicon-ui/blocks';
const message: ChatMessageData = {
id: 'custom-tool-1',
role: 'assistant',
parts: [
{
type: 'tool-call',
id: 'tc-weather',
name: 'get_weather',
state: 'complete',
input: { city: 'Berlin' },
output: { tempC: 7, condition: 'Overcast' }
},
{ type: 'text', text: "It's **7 °C** and overcast in Berlin right now." }
],
createdAt: new Date('2026-01-01T09:41:00'),
status: 'complete'
};
</script>
<!--
partRenderers swaps the built-in tool-call presentation for your own, keyed by
the part `type`. The snippet receives the fully-typed tool-call part.
-->
{#snippet toolCall(part: ChatToolCallPart)}
<div
class="border-border-subtle bg-surface-base rounded-modify flex items-center gap-2 border px-3 py-2 text-sm"
>
<span class="bg-success size-2 rounded-full"></span>
<span class="text-text-primary font-medium">{part.name}</span>
<span class="text-text-tertiary">→ {part.state}</span>
</div>
{/snippet}
<div class="w-full max-w-2xl">
<ChatMessage {message} layout="plain" partRenderers={{ 'tool-call': toolCall }} />
</div>
Error state with retry
Let me pull the latest figures for you
Retry pressed 0×
<script lang="ts">
import { ChatMessage, type ChatMessageData } from '@urbicon-ui/blocks';
// `status: 'error'` switches the message to its failure presentation: the
// partial text stays, and an Alert appears with a Retry button wired to onRetry.
let attempt = $state(1);
const message: ChatMessageData = {
id: 'error-1',
role: 'assistant',
parts: [{ type: 'text', text: 'Let me pull the latest figures for you' }],
createdAt: new Date('2026-01-01T09:41:00'),
status: 'error'
};
</script>
<div class="w-full max-w-2xl">
<ChatMessage
{message}
layout="plain"
onRetry={() => (attempt += 1)}
errorLabel="Couldn't reach the model"
/>
<p class="text-text-tertiary mt-2 text-xs">Retry pressed {attempt - 1}×</p>
</div>
02 Part dispatch
A message is an ordered list of parts. ChatMessage
renders them in order, choosing a renderer for each type.
The same component shows a plain answer or a full agentic transcript.
text→ rendered through StreamingMarkdown, with links checked against the URL policy.reasoning→ a collapsed ReasoningDisclosure with a "Thought for Xs" label.tool-call→ a ToolCallCard reflecting its pending / running / complete / error state.attachment→ a policy-checked chip linking to the file as a download, not inline media.source→ moved into the deduplicated citation footer and numbered as[n]markers.
Override any part type except source via partRenderers, and replace the avatar, action bar, or
metadata row through their snippets. ChatMessage never mutates the message you pass it.
03 Accessibility
Labelled actions
The copy and regenerate buttons carry aria-labels (copyLabel, regenerateLabel) and are wrapped in tooltips. They
live in a bar revealed on hover / focus-within. Keyboard users reach them by tabbing;
the reveal is visual only and does not trap focus.
Copy feedback
A successful copy is announced through a visually hidden role="status" region (the copiedLabel text), so screen-reader users get the confirmation
even without a visible toast.
Error & aborted alerts
status: 'error' and 'aborted' render through the Alert primitive, so the failure is exposed with the correct alert semantics rather than styled text
alone.
Decorative avatar & time
The role avatar is decorative and hidden from assistive tech; the timestamp renders as a <time datetime> element, so the exact instant is
machine-readable alongside the visible label.
04 API Reference
24 propsProp | Type | Default | Description | |
|---|---|---|---|---|
message required | ChatMessageData | — | The message to render. The component never mutates it. | |
abortedLabel | string | 'Generation stopped' | Alert title for status === 'aborted'. | |
actions | Snippet<[{ message: ChatMessageData }]> | — | Action-bar override. Receives the message; default is copy (+ regenerate when onRegenerate is set). | |
avatar | Snippet<[{ role: ChatRole }]> | — | Avatar override. Receives the message role; default is the Avatar primitive with a role icon. | |
class | string | — | Extra classes merged onto the root element. | |
copiedLabel | string | the `accessibility.copied` translation | Label shown briefly after a successful copy. | |
copyFailedLabel | string | the `accessibility.copyFailed` translation | Label shown briefly after a FAILED copy — e.g. a denied clipboard permission or a non-secure context. | |
copyLabel | string | the `accessibility.copy` translation | Accessible label / tooltip for the copy action. | |
density variant | comfortablecompact | comfortable | Controls the density behavior and appearance of the ChatMessage component. Available options: comfortable, compact. | |
errorLabel | string | 'Something went wrong' | Alert title for status === 'error'. | |
layout variant | bubbleplain | bubble | Controls the layout behavior and appearance of the ChatMessage component. Available options: bubble, plain. | |
metadata | Snippet<[{ message: ChatMessageData }]> | — | Metadata override. Receives the message; default is createdAt as a <time> element. | |
onRegenerate | () => void | — | Provide to render a "Regenerate" action. Called on click. | |
onRetry | () => void | — | Provide to render a "Retry" button in the error/aborted Alert. Called on click. | |
partRenderers | ChatPartRenderers | — | Per-part-type render overrides (see ChatPartRenderers). | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ ChatMessage: {...} }}>.
Prefer this over class overrides for reusable custom looks. | |
regenerateLabel | string | 'Regenerate' | Accessible label / tooltip for the regenerate action. | |
retryLabel | string | 'Retry' | Label for the retry button in the error/aborted Alert. | |
roleLabels | Partial<Record<ChatRole, string>> | — | Display names per role, shown in the plain-layout header. | |
slotClasses | Partial<Record<ChatMessageSlots, string>> | — | Per-slot class overrides. Slots: root | container | header | roleName | avatar |
column | bubble | partsFlow | attachment | attachmentIcon | attachmentName |
attachmentSize | sourcesFooter | placeholder | statusAlert | footer | actions |
actionButton | metadata. column wraps the bubble with everything under it
(citations, status alert, footer) and carries the role-dependent alignment —
override it to re-align a message's whole stack, not just the bubble.
Reasoning and tool-call parts render through ReasoningDisclosure /
ToolCallCard, which take their own slotClasses. | |
unstyled | boolean | — | Remove all default tv classes, from this component and the ones it renders (Avatar, Alert, Tooltip, the part renderers). | |
urlPolicy | MarkdownUrlPolicy | — | URL policy applied to attachment links and forwarded to StreamingMarkdown / CitationChip. Strict by default (untrusted LLM/server output). Keep it referentially stable to avoid re-parsing streamed markdown. | |
...ChatMessageVariants variant | VariantProps | — | Styling variants from ChatMessageVariants | |
...HTMLAttributes<HTMLDivElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'children' | 'class') |
05 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
ChatPartRenderers | type | helper | 1 | Per-part snippet overrides keyed by part type. When a renderer exists for a
part's type it replaces the built-in rendering for that part (one level up
from StreamingMarkdown's node renderers) — this is how P3 swaps in
ToolCallCard / ReasoningDisclosure and P4 adds A2UIView without touching this
component. source is intentionally not overridable here: sources are
collected into the citation footer, not rendered inline. | |
ChatMessageProps | interface | props | 0 | — | |
ChatMessageData | interface | helper | 1 | One message in a conversation. Named ChatMessageData because the value
export ChatMessage is the component that renders it. | |
ChatMessagePart | type | helper | 0 | One renderable segment of a message. Mirrors the shape of modern model/tool transcripts: interleaved text, reasoning, tool calls, sources and attachments — rendered in order by the ChatMessage component. | |
ChatRole | type | helper | 0 | Author of a chat message. | |
MarkdownUrlPolicy | interface | helper | 1 | — | |
ChatMessageSlots | type | variant | 0 | Slot names derived from the tv() config — single source of truth for slotClasses. | |
ChatMessageVariants | type | variant | 0 | — | |
ChatMessageStatus | type | helper | 0 | Lifecycle of a message. streaming drives the live-rendering affordances
(markdown tail repair, cursor, deferred screen-reader announcement);
error / aborted switch the message to its failure presentation.
A message without a status counts as complete. | |
CitationSource | interface | helper | 0 | A cited source surfaced behind a [id] citation marker. id keys the
source to its marker; title is always shown, url / snippet are optional
and only render when present (and, for url, when the URL policy allows it). | |
SlotNames | type | helper | 0 | Extracts the slot-name union from a slotted tv() config function — the
companion to VariantProps. The slot-mode overload returns
(props?) => { [K in keyof S]: SlotFn }, so keyof ReturnType<T> is exactly
the set of slot names a component declares in tv({ slots: … }).
Use it to type a component's slotClasses prop from the single source of
truth (its *.variants.ts) instead of hand-maintaining a parallel union
that silently drifts when a slot is added or renamed: | |
VariantProps | type | helper | 1 | — |
06 Installation
Import
import { ChatMessage } from '@urbicon-ui/blocks';
import type {
ChatMessageData,
ChatMessagePart,
ChatToolCallPart,
ChatReasoningPart,
CitationSource
} from '@urbicon-ui/blocks';