Message

Message.Root is one turn of a conversation, as an <article>. from says who wrote it: a user's message sits at the end side in a bubble, an assistant's runs the full width, and a system message is a quiet line in the middle. It knows nothing about any agent SDK's message shape: map your own data onto its parts.

On this page
<Message.Root from="user" aria-label="You">
  <Message.Content>How do I name an icon-only button?</Message.Content>
</Message.Root>

<Message.Root aria-label="Assistant">
  <Message.Avatar>
    <Avatar.Root tone="category-3">
      <Avatar.Fallback>A</Avatar.Fallback>
    </Avatar.Root>
  </Message.Avatar>
  <Message.Header>
    <strong>Assistant</strong>
    <Message.Time value={sentAt} locale={locale} />
  </Message.Header>
  <Message.Content>
    <Prose.Root>
      <p>Give it aria-label, or put a visually hidden label inside it.</p>
    </Prose.Root>
  </Message.Content>
</Message.Root>
Conversation started
How do I name an icon-only button?
Assistant

Give it aria-label, or put a visually hidden label inside it. The icon itself stays hidden from screen readers.

Server Components can import it from @tounsoo/todae/message.

Parts

Every part but Message.Time is a div, and all of them are optional.

  • Message.Avatar holds an Avatar.Root. With one, the other parts sit in a column beside it: after it for an assistant or system message, before it for a user's.
  • Message.Header is a row for the author's name and a Message.Time. A heading or paragraph directly inside it has no margin.
  • Message.Content is the text. For a user's message it is the bubble. Put rendered markdown in a Prose root inside it. A message can hold more than one, with other elements between them, such as the calls of the Tool call recipe or the mixed reply in Message parts. The Citations recipe adds numbered source markers and a sources list to it.
  • Message.Time is a <time> that says when the message was sent. It can sit in the header or in the actions row.
  • Message.Actions is a row for the message's actions, such as copy and retry. Put a Toolbar in it when there are several, so they take one tab stop. With reveal="hover", the row shows only while the pointer is over the message or focus is inside it; on a screen with no hover, such as a phone, it always shows. The Message actions recipe puts copy, regenerate, rating and edit buttons in it.

from sets data-from and changes only layout and tokens: it does not change the role or what screen readers hear.

Time

Message.Time takes the time as a Temporal.ZonedDateTime, so you choose the time zone it is shown in. It writes dateTime as the instant in UTC, such as 2026-10-14T00:41:00Z, and the text in the value's time zone, in locale's short time style and the Gregorian calendar, such as 9:41 AM in en-US and 오전 9:41 in ko. Set dateStyle or timeStyle for another style, or pass children to show your own text, such as a relative time from formatRelativeTime; dateTime still comes from value.

const sentAt = Temporal.Instant.from(message.createdAt).toZonedDateTimeISO(timeZone);

<Message.Time value={sentAt} locale={locale} dateStyle="medium" timeStyle="short" />

Without locale, the runtime's own locale is used. When a server renders the message, pass the reader's locale and time zone, so the text doesn't depend on the server's own.

Streaming

Set streaming while the message is still arriving, and set it back to false when it finishes or fails. It sets aria-busy="true" on the article and data-streaming, and draws nothing of its own. To show that the reply is still coming, add Loader's dots variant, as below.

<Message.Root streaming={status === 'streaming'} aria-label="Assistant">
  <Message.Content>
    <Prose.Root>{rendered}</Prose.Root>
  </Message.Content>
  {status === 'streaming' && (
    <Message.Actions>
      <Loader.Root variant="dots" aria-label="Writing" />
    </Message.Actions>
  )}
</Message.Root>

Two steps:

  • Pick a variant
  • Give it a name

Restyle it through the message tokens: message.fg sets the text color, message.gap and message.row-gap space the parts, message.header.* sets the header's color, size and the smaller space below it, message.time.* the time's color and size, message.actions.motion.* how a reveal="hover" row fades, message.user.* the bubble's background, text color, radius, padding, max-width and the border-width of its forced-colors outline, and message.system.* the system line. A Prose.Root inside keeps the prose tokens' text color and size.

Accessible composition

Todae handles: An <article> for each message, so screen reader users can move between them. aria-busy while it streams. In forced colors the user bubble keeps an outline, since its background is replaced. Message.Time writes a machine-readable dateTime. A reveal="hover" row shows when focus moves into the message, so keyboard users don't land on hidden buttons.

You supply: A name for each message that says who wrote it, such as aria-label="You" or aria-labelledby pointing at the author in Message.Header. Names for icon-only action buttons. One announcement when a message finishes, rather than the streamed text; inside a Conversation, it makes that announcement.

Watch for: Putting the message that is streaming in a live region: every token is read as it arrives. Hiding the actions of the latest reply with reveal="hover": they are the ones people use most. Telling user and assistant apart by color or side alone; the name says it too.

When to use

When: The turns of a chat with a person or an assistant, comments in a thread, a support conversation.

When not: A one-off status message about the page → Alert. A notice that comes and goes → Toast. A bordered group of content → Card.

Props

Message.Root props
NameTypeDescription
from"user" | "assistant" | "system"

Who wrote the message. Sets data-from, which sets layout and tokens only.

Default: "assistant"

streamingboolean

True while the message is still arriving. Sets aria-busy and data-streaming. Set it back to false when the message finishes or fails.

Default: false

childrenReact.ReactNode

Message parts: Avatar, Header, Content and Actions.

Message.Avatar props
NameTypeDescription
childrenReact.ReactNode

An Avatar.Root.

Message.Header props
NameTypeDescription
childrenReact.ReactNode

The author's name and, if any, a Message.Time.

Message.Content props
NameTypeDescription
childrenReact.ReactNode

The text, often a Prose.Root with rendered markdown.

Message.Time props
NameTypeDescription
value RequiredTemporal.ZonedDateTime

When the message was sent, in the time zone to show it in.

localestring

The locale to write it in, such as "ko". The runtime's own when not set, so set it when a server renders the message.

dateStyle"full" | "long" | "medium" | "short"

Intl's dateStyle. With neither style set, the time is written in the short time style.

timeStyle"full" | "long" | "medium" | "short"

Intl's timeStyle. With neither style set, it is short.

childrenReact.ReactNode

Text to show in place of the formatted value, such as "2 min ago". dateTime still comes from value.

Message.Actions props
NameTypeDescription
reveal"always" | "hover"

hover hides the row until the pointer is over the message or focus is inside it. On a screen with no hover, such as a phone, it always shows.

Default: "always"

childrenReact.ReactNode

Buttons, or a Toolbar.Root holding them.