Message
One chat row per role — alignment, surface, and consecutive-row grouping all derive from a single from prop.
<Message from="user"> <MessageContent>Which clients still need their annual review?</MessageContent></Message><Message from="assistant"> <MessageContent>Three reviews are due this month: Hargreaves, Patel, and Osei.</MessageContent></Message><Message from="system"> <MessageContent>Conversation moved to the Hargreaves client file</MessageContent></Message>Usage
import { Message, MessageContent } from "@reva/ui";Everything derives from from. The markup is identical for every role — a MessageContent (plus any below-content chrome — an Actions bar and a timestamp, arranged by MessageFooter) — and the role drives alignment and surface. Rows are deliberately avatar-less and quiet, so a conversation reads as a document rather than a support chat:
user— right-aligned, quiet neutral bubble (surface/subtle-solid).assistant— left-aligned and unwrapped — no card, no padding; the reply reads as page content at full row width.system— full-width, de-emphasized small text. For error rows, compose an Alert insideMessageContent(see the example below).
Rows must be direct siblings in a no-gap container
Grouping and rhythm are pure sibling CSS: each row adds mt-6 after a role change and mt-2 after a same-role row. That assumes message rows are direct siblings in a container without its own gap — ConversationContent is exactly that, and a VStack gap={0} works standalone. A gap-* on the list would stack with the rows' margins.
Rows enter with a small fade and rise (reduced-motion safe, AnimatePresence-compatible). When hydrating an existing history — like the seeded demos on the Conversation page — pass initial={false} so old messages don't replay their entrance.
Composition
Use the following composition to build a Message:
Message (from="user" | "assistant" | "system")
├── MessageContent (plain text, or composed parts: Response, CodeBlock, Sources, …)
└── MessageFooter? (optional below-content row — space-between)
├── Actions (copy, regenerate, vote, …)
└── MessageTimestamp (the send / generated time)MessageContent is the family's composition seam — it renders whatever parts you compose: a Response, a Code Block, Sources, a Loader, or plain text. MessageFooter is the below-content row that pushes an Actions bar and a MessageTimestamp to opposite edges; MessageTimestamp locks the quiet trailing-meta voice (caption1, regular weight, muted ink) so timestamps never drift in style.
Examples
Consecutive-role grouping
Same-role rows group automatically — tight mt-2 gap after a same-role row, mt-6 after a role change. No state, no list-walking: the row never knows about its siblings, the previous-sibling CSS does.
<Message from="user">…</Message><Message from="user">…</Message> {/* groups: mt-2 */}<Message from="assistant">…</Message> {/* new group: mt-6 */}<Message from="assistant">…</Message>Message footer
Below an assistant reply, wrap the Actions bar and a MessageTimestamp in MessageFooter — justify-between pushes them to opposite edges. Under a user message, drop a bare MessageTimestamp: the row's right-alignment carries it under the bubble.
<Message from="assistant">
<MessageContent>Three reviews are due this month: Hargreaves, Patel, and Osei.</MessageContent>
<MessageFooter>
<Actions>{/* copy, regenerate, vote */}</Actions>
<MessageTimestamp>Jun 13, 1:29pm</MessageTimestamp>
</MessageFooter>
</Message>
<Message from="user">
<MessageContent>Which clients still need their annual review?</MessageContent>
<MessageTimestamp>Jun 13, 1:28pm</MessageTimestamp>
</Message>System error row
System rows are the slot for conversation-level notices. For failures, compose the house Alert inside MessageContent — the row stays full-width and the Alert brings its own status semantics.
<Message from="system"> <MessageContent> <Alert status="destructive"> <AlertIcon /> <AlertTitle>Reply failed — the model timed out. Try regenerating.</AlertTitle> </Alert> </MessageContent></Message>Accessibility
- Message rows carry no live-region semantics of their own — announcing new rows is the scroll shell's job. Inside Conversation,
ConversationContentis therole="log"polite live region; that page's Accessibility section covers how additions and streamed text are announced. - Rows are avatar-less by design — the author is conveyed by the role-driven alignment and surface (right-aligned bubble vs. unwrapped left column), so no decorative imagery needs hiding from assistive tech.
- Under reduced motion the entrance fade/rise is skipped (values only — the markup never forks). The
initial/animate/exit/transitionoverrides are trusted as passed, so keep overrides reduced-motion-aware. - System rows are plain de-emphasized text with no implicit status semantics — for failures, compose the house Alert inside
MessageContent(example above) and it brings its ownrole.
Props
| Component | Prop | Type | Default | Description |
|---|---|---|---|---|
Message | from | "user" | "assistant" | "system" | — | Author role — drives alignment, surface, and grouping. Required. |
Message | initial / animate / exit / transition | Motion props | fade + rise on the house spring | Override the entrance motion; initial={false} for hydrated history. |
MessageContent | — | div props | — | The composition seam — renders whatever parts you compose. |
MessageFooter | — | div props | — | Below-content row; flex + justify-between to push Actions (left) and the timestamp (right) apart. |
MessageTimestamp | — | Text props (minus textStyle / weight / color) | — | The send / generated time. Locks the caption1 / regular / muted recipe; pass the formatted string as children. |