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>
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.Avatarholds anAvatar.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.Headeris a row for the author's name and aMessage.Time. A heading or paragraph directly inside it has no margin.Message.Contentis 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.Timeis a<time>that says when the message was sent. It can sit in the header or in the actions row.Message.Actionsis 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. Withreveal="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
| Name | Type | Description |
|---|---|---|
from | "user" | "assistant" | "system" | Who wrote the message. Sets data-from, which sets layout and tokens only. Default: "assistant" |
streaming | boolean | 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 |
children | React.ReactNode | Message parts: Avatar, Header, Content and Actions. |
| Name | Type | Description |
|---|---|---|
children | React.ReactNode | An Avatar.Root. |
| Name | Type | Description |
|---|---|---|
children | React.ReactNode | The author's name and, if any, a Message.Time. |
| Name | Type | Description |
|---|---|---|
children | React.ReactNode | The text, often a Prose.Root with rendered markdown. |
| Name | Type | Description |
|---|---|---|
value Required | Temporal.ZonedDateTime | When the message was sent, in the time zone to show it in. |
locale | string | 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. |
children | React.ReactNode | Text to show in place of the formatted value, such as "2 min ago". dateTime still comes from value. |
| Name | Type | Description |
|---|---|---|
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" |
children | React.ReactNode | Buttons, or a Toolbar.Root holding them. |