Message

Message.Root는 대화의 한 차례로, <article>입니다. from은 누가 썼는지 나타냅니다. 사용자의 메시지는 끝쪽에 말풍선으로 놓이고, 어시스턴트의 메시지는 너비 전체를 쓰며, 시스템 메시지는 가운데의 조용한 한 줄입니다. 어떤 에이전트 SDK의 메시지 형태도 알지 못하므로, 자신의 데이터를 파트에 맞춰 넣으세요.

이 페이지의 내용
<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는 @tounsoo/todae/message에서 가져올 수 있습니다.

파트

Message.Time을 뺀 모든 파트는 div이고, 모두 넣지 않아도 됩니다.

  • Message.Avatar에는 Avatar.Root를 넣습니다. 이것이 있으면 다른 파트는 그 옆에 한 열로 놓입니다. 어시스턴트나 시스템 메시지에서는 그 뒤에, 사용자의 메시지에서는 그 앞에 놓입니다.
  • Message.Header는 작성자 이름과 Message.Time을 담는 줄입니다. 바로 안의 제목이나 문단은 margin이 없습니다.
  • Message.Content는 본문입니다. 사용자의 메시지에서는 이것이 말풍선입니다. 렌더링한 markdown은 그 안의 Prose root에 넣으세요. 한 메시지에 여러 개를 두고, Tool call 레시피의 호출이나 Message parts 레시피의 섞인 답처럼 다른 요소를 그 사이에 넣어도 됩니다. Citations 레시피는 여기에 번호 붙은 출처 표시와 출처 목록을 더합니다.
  • Message.Time은 메시지를 보낸 시각을 나타내는 <time>입니다. 헤더에 두어도 되고 동작 줄에 두어도 됩니다.
  • Message.Actions는 복사나 다시 시도처럼 메시지에 대한 동작을 담는 줄입니다. 동작이 여러 개면 Toolbar에 넣어 탭 정지를 하나로 만드세요. reveal="hover"를 주면 포인터가 메시지 위에 있거나 포커스가 그 안에 있을 때만 보입니다. 휴대폰처럼 hover가 없는 화면에서는 늘 보입니다. Message actions 레시피는 복사, 다시 생성, 평가, 편집 버튼을 이 줄에 넣습니다.

from은 data-from을 설정하고 레이아웃과 토큰만 바꿉니다. role이나 스크린 리더가 듣는 내용은 바꾸지 않습니다.

시각

Message.Time은 시각을 Temporal.ZonedDateTime으로 받으므로, 어느 시간대로 보여 줄지 직접 고릅니다. dateTime에는 2026-10-14T00:41:00Z처럼 UTC 시각을 쓰고, 텍스트는 값의 시간대에서 locale의 짧은 시각 스타일과 그레고리력으로 씁니다. en-US에서는 9:41 AM, ko에서는 오전 9:41입니다. 다른 스타일은 dateStyle이나 timeStyle로 정하고, formatRelativeTime으로 쓴 상대 시각처럼 직접 정한 텍스트를 보여 주려면 children을 넘기세요. 그래도 dateTime은 value에서 옵니다.

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

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

locale이 없으면 런타임의 locale을 씁니다. 서버가 메시지를 렌더링한다면 읽는 사람의 locale과 시간대를 넘겨서, 텍스트가 서버의 locale과 시간대를 따르지 않게 하세요.

스트리밍

메시지가 아직 도착하는 동안 streaming을 주고, 끝나거나 실패하면 false로 되돌리세요. article에 aria-busy="true"와 data-streaming을 설정하고, 그 밖에는 아무것도 그리지 않습니다. 답이 아직 오고 있음을 보여 주려면 아래처럼 Loader의 dots 변형을 넣으세요.

<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

message 토큰으로 바꾸세요. message.fg는 텍스트 색, message.gap과 message.row-gap은 파트 사이 간격, message.header.*는 헤더의 색, 크기와 그 아래의 더 좁은 간격, message.time.*는 시각의 색과 크기, message.actions.motion.*는 reveal="hover" 줄이 나타나는 모션, message.user.*는 말풍선의 배경, 텍스트 색, 모서리, 패딩, max-width, 강제 색상 모드 외곽선의 border-width, message.system.*는 시스템 줄을 정합니다. 안에 넣은 Prose.Root는 prose 토큰의 텍스트 색과 크기를 그대로 씁니다.

접근성을 지키는 조합

Todae가 맡는 것: 메시지마다 <article>이므로 스크린 리더 사용자가 메시지 사이를 이동할 수 있습니다. streaming 동안 aria-busy를 줍니다. 강제 색상 모드에서는 배경이 바뀌므로 사용자 말풍선에 외곽선이 남습니다. Message.Time은 기계가 읽을 수 있는 dateTime을 씁니다. reveal="hover" 줄은 포커스가 메시지 안으로 들어오면 보이므로, 키보드 사용자가 숨은 버튼에 머무르지 않습니다.

직접 제공할 것: 누가 썼는지 말해 주는 각 메시지의 이름. aria-label="You"나, Message.Header의 작성자를 가리키는 aria-labelledby를 쓰세요. 아이콘만 있는 동작 버튼의 이름. streaming되는 텍스트 대신 메시지가 끝날 때 한 번 하는 알림. Conversation 안에서는 Conversation이 알립니다.

주의할 점: streaming 중인 메시지를 live region에 넣는 것. 토큰이 도착할 때마다 읽힙니다. 가장 최근 답의 동작을 reveal="hover"로 숨기는 것. 사람들이 가장 많이 쓰는 동작입니다. 사용자와 어시스턴트를 색이나 놓인 쪽만으로 구분하는 것. 이름으로도 말해야 합니다.

언제 쓰나요

쓸 때: 사람이나 어시스턴트와 나누는 채팅의 차례, 스레드의 댓글, 고객 지원 대화.

쓰지 않을 때: 페이지에 대한 한 번의 상태 메시지 → Alert. 나타났다 사라지는 알림 → Toast. 테두리로 묶은 내용 → Card.

Props

Message.Root props
이름타입설명
from"user" | "assistant" | "system"

메시지를 쓴 쪽. data-from을 설정하며, 이는 레이아웃과 토큰만 정합니다.

기본값: "assistant"

streamingboolean

메시지가 아직 도착하는 중이면 true. aria-busy와 data-streaming을 설정합니다. 메시지가 끝나거나 실패하면 false로 되돌리세요.

기본값: false

childrenReact.ReactNode

메시지 파트: Avatar, Header, Content, Actions.

Message.Avatar props
이름타입설명
childrenReact.ReactNode

Avatar.Root.

Message.Header props
이름타입설명
childrenReact.ReactNode

작성자 이름과, 있다면 Message.Time.

Message.Content props
이름타입설명
childrenReact.ReactNode

텍스트. 렌더링한 markdown을 담은 Prose.Root인 경우가 많습니다.

Message.Time props
이름타입설명
value 필수Temporal.ZonedDateTime

메시지를 보낸 시각. 보여 줄 시간대로 줍니다.

localestring

시각을 쓸 locale. 예: "ko". 없으면 런타임의 locale을 쓰므로, 서버가 메시지를 렌더링한다면 주세요.

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

Intl의 dateStyle. 두 스타일 모두 없으면 시각을 짧은 시각 스타일로 씁니다.

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

Intl의 timeStyle. 두 스타일 모두 없으면 short입니다.

childrenReact.ReactNode

포맷한 값 대신 보여 줄 텍스트. 예: "2분 전". 그래도 dateTime은 value에서 옵니다.

Message.Actions props
이름타입설명
reveal"always" | "hover"

hover는 포인터가 메시지 위에 있거나 포커스가 그 안에 있을 때까지 줄을 숨깁니다. 휴대폰처럼 hover가 없는 화면에서는 늘 보입니다.

기본값: "always"

childrenReact.ReactNode

버튼, 또는 버튼을 담은 Toolbar.Root.