Message
Shows one turn of a conversation: an agent turn as flow with no drawn header, a person turn as a bubble at the end of the row.
Examples
Interrupted
Context compacted
AgentHanded to billing-agent
Handed to Ana
From
agentdefault
user
State
live
waiting
fail
stopped
donedefault
Reason
- Interrupted
interrupted
- Context compacted
compaction
- Handed to Ana
handoff
- Tuesday 30 September
day
States
sending
failed
sent
Usage
When to use it
- Use
- A thread shows turns from a person and an agent in order.
- Use
- The thread needs a marked change: a stop, a compaction, a handoff or a new day.
- Not for
- A transient notice about the page, not a turn. Use Alert.
- Not for
- The scrolling container of the whole thread. Use Conversation.
- Not for
- The agent's answer body with Markdown. Use Response.
Rules
9 rules
- Must
- Give every agent turn a MessageHeader that carries one MessageStatus. Because the footer hides while the turn is live and the header ends in Failed or Stopped, all read from that one status.
- Must
- Set a person's turn as a paper bubble at the end of the row, with the frame line; no avatar and no drawn name. Because the bubble at the end against the full-width flow of the agent is what tells the two speakers apart, in grayscale too.
- Must
- Name the speaker in MessageHeader text on both turns; a person's header is read aloud and not drawn. Because the bubble has no avatar or name beside it, so a screen reader hears the turn with no speaker.
- Must
- End a stopped turn with MessageStatus state="stopped" and a MessageDivider reason="interrupted", and keep the partial answer. Because a red Alert reads the person's own Stop as a failure, and dropping the partial answer loses work they may need.
- Must
- Keep a failed message in its row with MessageSendState state="failed" and onRetry. Because a toast fades in seconds and the person has to retype what they wrote.
- Must
- Pass both models when the answering model differs from the picked one. Because an operator reads a fallback model's answer as the picked model's and misjudges its quality.
- Never
- Put the agent's turn in a bubble or a frame. It is flow: sections and a footer, with no name line or avatar. Because a bubble squeezes code blocks and tool rows, and a frame around a whole turn nests the approval card and the code wells in a second box.
- Never
- Put a Working or Paused word, a badge or the model in the agent header. The running tool row is the live state; the model goes in the footer. Because a word in the header and the arc in a row say the same thing twice, and the header word goes stale the moment the turn waits on an approval.
- Should
- Use the reason's default words, or 2 to 5 words in sentence case: "Handed to Ana", "Back to support-agent".
Accessibility
4 checks
- Tab moves to footer actions, bubble buttons and links; turns are not stops.
- Enter on Retry in the failed state resends and the button leaves, so repeats cannot double-send.
- The turn state and the send state read as words; the wash is never the only quartz.
- MessageHeader names the speaker aloud on both turns; an agent's is drawn only when the turn failed or stopped.
Edge cases
8 edge cases
- Long message and hash
- Wraps at the reading measure; a 40-character hash breaks anywhere.
- Narrow screen
- Both turns keep a 16px inset; a bubble grows to 85% of the row, and a failed or stopped header keeps one line.
- Right to left
- The agent flow starts from the right and the bubble moves to the left; the time column stays at the end.
- Empty turn
- Nothing is drawn; the name is read aloud, and the thread's thinking row shows the first wait.
- Slow send
- The row stays and says Sending, with no spinner on the text.
- Failed send
- Keeps the text, washes the row and offers Retry in place.
- Reduced motion
- The row arc holds still; the row says Running.
- Zoom 200%
- Sections grow with the text; a drawn header keeps 24px as a minimum.
API
12 props
Message from, kind, size'agent' | 'user'AgentTurn name, time, statestring, ReactNode, MessageStatus statedefaultstate 'done'MessageStatus state, label'live' | 'waiting' | 'fail' | 'stopped' | 'done'MessageTimetimeMessageBubble variant, kinddefault, outline, inverse, ghost, destructive, tinteddefault'default'MessageBubble status'fail'MessageBubbleContent renderReactElementMessageWorked duration, time, labelsstring, ReactNodeMessageModel model, requestedstringMessageSendState state, onRetry'sending' | 'sent' | 'failed'MessageDivider reason, kind, render'interrupted' | 'compaction' | 'handoff' | 'day'labelsper partdefaultEnglish words
Tokens
12 tokens
--line-frame- oklch(var(--ink-c) / var(--line-frame-alpha))
--line-row- oklch(var(--ink-c) / 0.08)
--line-control- oklch(var(--ink-c) / 0.16)
--fail-text- oklch(51% 0.180 26)
--fail-wash- oklch(94.5% 0.027 26)
--surface-inverse- var(--ink)
--text-inverse- var(--paper)
--type-label- 400 14px/16px var(--font-sans)
--type-tag- var(--type-code-sm)
--type-body-sm- 380 13px/18px var(--font-sans)
--text-3- oklch(var(--ink-c) / 0.66)
--time-column- 12ch