Skip to main content

Chat

Message

One message in a chat: who wrote it, their avatar and what they wrote, with any attachments under it. role places and colors it: the user's own messages at the end in the primary color, the assistant's at the start, the app's system notes centered. streaming marks a reply still being written. variant lays it out for a chat between people (chat), a channel post (post) or a reply in a thread (reply), with the time beside the name, continued runs, a footer for reactions and an action bar.

Ana Lind
How long should the dough rest?
Ouro
About an hour, covered, somewhere warm. It is ready when it has doubled.
Mara Quill,
The venue confirmed the long table for Friday.
Mara Quill,
Can someone bring the extension cord?
Halden, AI agent,
I'll post the summary of the launch notes by 4:00.
Priya Okafor,
Quoting Mara Quill: Can someone bring the extension cord?
I will bring it.
Seen

On this site

This site's own pages use Message too. Open one with X-ray pointing it out: The Nautilus's story

Install

Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):

npx shadcn@latest init --base base --preset nova --yes --no-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10

Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):

npx shadcn@latest add ourostack/ouro-ui/message#v1.3.10

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/message

Its tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):

npx shadcn@latest add ourostack/ouro-ui/message#v1.3.10 ourostack/ouro-ui/message-tests#v1.3.10
import { Message } from "@/components/ouro/message/message";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<Message role="user" name={user.name} avatar={user.photoUrl}>How long do I rest the dough?</Message>
<Message role="assistant" name="Ouro" streaming={!reply.done}>{reply.text}</Message>
<Message variant="chat" role="assistant" name="Grace" time="9:41 AM" footer={<ReactionBar reactions={reactions} />} actions={[{ key: "reply", label: "Reply", icon: <ReplyIcon />, onClick: reply }]}>Lunch?</Message>
<Message variant="post" role="assistant" name="Grace" time="9:41 AM" subject="Launch plan" thread={<ThreadSummary count={2} onOpen={open} />}>Notes inside.</Message>

Usage

Use it inside MessageList, which keeps the newest message in view and announces new ones.

Use instead when

  • Use Alert instead for a notice that is not part of a conversation, and toast() for a passing confirmation.

Props

The props the modes share. Type: MessageProps.

actions
Type: readonly MessageAction[]
The message's actions, as icon buttons with tooltips in a small bar at the bubble's top end (actionsPlacement) or over a post's top end. The bar shows while the pointer is over the message, while focus is in it, and after a tap on a touch screen. It is a toolbar: one Tab stop, with the arrow keys moving between its buttons. In a MessageList the list draws one bar for all its messages, on the message the pointer is over (or that keyboard focus is in, or was tapped), and hides it as the pointer leaves, the list scrolls under it, the window loses focus or the menu opens, so at most one bar shows and none stays behind. Escape hides it.
actionsLabel
Type: string
Default: "Message actions"
What screen readers hear for the action bar, for other languages.
actionsMenuLabel
Type: string
Default: Message actions for ${name}
What screen readers hear for the menu a MessageList opens for the message.
actionsPlacement
Type: "corner" | "beside"
Default: "beside" on a reply, "corner" on a chat
Where the action bar sits on a chat or reply message. beside puts it just past the bubble's end, level with its top, so it covers neither the name nor the message and stays with the bubble however wide the row; where the row has no room left, it ends at the row's edge, over the bubble's corner. On the user's own chat message it sits before the bubble. corner puts it over the bubble's top corner, raised over the line above. A post's bar is over its top end, and the default layout's over its bubble's corner.
attachments
Type: ReactNode
Files and pictures sent with the message, shown under it: <Attachment> elements, and pictures as <Lightbox layout="mosaic">, which keeps them to a chat's size and opens them full size.
avatar
Type: string | false | ReactElement
The author's picture URL. Without it the avatar shows initials; false shows no avatar. For the author's presence, pass presence. Pass an Avatar element instead for a square avatar or a color of your own: avatar={<Avatar name="Ana" src={url} alt="" size="sm" />} (alt="", since the name is written beside it). In chat, the user's own messages show no avatar.
bubble
Type: "filled" | "raised"
Default: "filled" on a chat message; unboxed on a reply
How a chat message's bubble looks. filled, the default, is a messaging app's: the user's own message filled as a primary button is (--ouro-action, else the primary color), others' on the muted surface, each round with a smaller corner toward its sender, with an edge that forced colors draw. Inside the user's own bubble, the theme's colors follow the fill, so a link (underlined), a mention, a quote, a button or a focus ring in it stays readable on it; on others', a link or mention in the primary color is a shade nearer the text color, so it keeps 4.5:1 on the muted surface. raised is a workplace chat's: others' messages on a raised card with a hairline and a small shadow, the user's own in a tint of the primary color. A reply with a bubble is laid out as a chat's message, for a thread that reads as a chat: the user's own at the end, others' at the start under their name, each in a bubble of that look. A reply without one stays unboxed, and default and post ignore it.
children
Type: ReactNode
The message: text, or rich content such as a list or a code block.
className
Type: string
Layout only: margins (mt-4). Appearance classes warn in development; the colors follow role.
continued
Type: boolean
The message follows one from the same author: no avatar and no name line, so a run of messages reads as one. Its time shows beside it on hover and focus. Screen readers still hear the name and time. Not for post: every post shows its author.
dateTime
Type: string
The same moment for machines, as an ISO date (2026-05-01T09:41): the <time> element's dateTime.
deleteLabel
Type: string
Default: "Delete"
The name of a failed message's delete button, for other languages.
failed
Type: MessageFailed | false
The message did not send, and what the person can do about it: { onRetry, onDelete }. A calm marker under it says "Not sent" beside Retry and Delete buttons, and stays until the app clears failed, so a failed send never goes quiet. Screen readers hear the marker as the message's description. It appears at once, with no motion. To put the text back in a Composer instead, set the Composer's value to it and delete the message.
failedLabel
Type: string
Default: "Not sent"
What the marker under a failed message says, for other languages.
footer
Type: ReactNode
Under the message: its reactions (<ReactionBar>), "Edited", whether it was delivered or seen.
menu
Type: readonly MenuEntry[]
More actions, in a menu that opens from the last button in the action bar: the same entries as Menu's items, each with an optional icon before its label and its shortcut at its end. The bar stays while the menu is open. The bar renders in the browser once its code loads, so server HTML has the message without it; Menu's code loads as soon as a bar with a menu is on the page, so the first press opens it without a wait. In a MessageList, the list has one menu for all its messages: the bar's menu button, a right-click on the message (except on a field, a link, a picture or selected text, which keep the browser's menu) and Shift+F10 or the context-menu key on it all open it, named by actionsMenuLabel, and closing it puts focus back on the message.
menuLabel
Type: string
Default: "More actions"
The menu button's name, for other languages.
name (required)
Type: string | null
The author's name. Screen readers hear it before the message, and the avatar's initials come from it. Required, so every message says who wrote it. The user's own name is read but not shown. Pass null while the name is still loading: a name-sized Skeleton line takes the name's place on its line, the avatar shimmers in its own shape, and screen readers hear nameLoadingLabel. Nothing moves when the name arrives.
nameContent
Type: ReactNode
What shows as the author's name in place of name, when plain text cannot draw it: their name with its emoji as pictures (<EmojiText text={person.name} catalogue={catalogue} />). name stays what screen readers hear and the avatar's initials come from.
nameLoadingLabel
Type: string
Default: "Name loading"
What screen readers hear while name is null, for other languages.
nameTag
Type: string
A quiet tag after the author's name, such as "AI agent" for an agent or "Guest": small and muted beside the name, not a badge, and read after it ("Halden, AI agent"). Where the name is hidden (a continued message, the user's own chat messages) the tag is too, and both are still read.
quickReactions
Type: readonly MessageAction[]
Reactions at the top of the message's menu, as a row of emoji: each an action whose icon is the emoji and whose label names it ("Like"). Left and right move along the row. Put one that opens a picker of every reaction last ("More reactions").
quickReactionsLabel
Type: string
Default: "Reactions"
What screen readers hear for the row of reactions in the menu, for other languages.
quote
Type: MessageQuote
The message this one replies to, at the top of the bubble or post, above the text: a bar, the author and a line of what they wrote, as Composer's quote shows it before the reply is sent. Screen readers hear it first, as "Quoting Ana: …". With onActivate it is a button, to show the quoted message.
ref
Type: Ref<HTMLElement>
Reaches the rendered element.
retryLabel
Type: string
Default: "Retry"
The name of a failed message's retry button, for other languages.
role (required)
Type: "user" | "assistant" | "system"
Who wrote it. user is the person using the app: their messages sit at the end, in the primary color. assistant is anyone else, such as an agent, at the start. system is the app's own note ("Ana joined"), centered and quiet.
streaming
Type: boolean
The reply is still being written. The message is aria-busy, an empty one shows a typing indicator, and MessageList announces it once, when streaming ends.
streamingLabel
Type: string
Default: "Writing"
What screen readers hear for the typing indicator, for other languages.
subject
Type: ReactNode
A post's subject, in semibold above its text. post only.
thread
Type: ReactNode
Under a post, or a message in a channel without threads: its thread, as a <ThreadSummary>.
time
Type: string
When it was written, already formatted for the reader ("9:41 AM"), shown after the name. Format it in the browser after hydration, so the server and the reader's clock agree.
variant
Type: "default" | "chat" | "post" | "reply"
Default: "default"
How the message is laid out. default suits a conversation with an assistant. chat suits people writing to each other: others' messages at the start under their name and time, the user's own at the end with only the time, each bubble at most 85% of the row's width and 36rem, so a line stays readable in a wide window. chat also suits a channel without threads, which reads as a chat. post is a post in a threaded channel, unboxed: the author's name and time, an optional subject, the text, then the footer and the thread under it, with room around it and a hairline from the text's start to the end between it and a post just before it. bubble sets how a chat message's bubble looks, and draws a reply as one. reply is a reply in a thread: the same line of name and time, with the text unboxed under it, starting where a post's text starts. In post and reply the user's own messages sit at the start too.

Modes

MessageProps is a union: pass the props of exactly one mode. Mixing them is a type error.

With presence and presenceLabel

presence (required)
Type: "offline" | "available" | "busy" | "away" | "do-not-disturb" | "out-of-office"
The author's presence, as Avatar's dot on the avatar's lower end corner: available, busy, away, do not disturb, offline or out of office, each in its own shape and tone. It shows wherever the avatar does, so not on a continued message or the user's own chat messages. With an Avatar element as avatar, pass presence to that Avatar instead.
presenceLabel (required)
Type: string
What the dot says, such as "Busy" or "In a meeting". Screen readers hear it on the avatar, as Avatar reads a presence beside a written name. Required with presence, so no dot is silent.

With presenceLabel

presenceLabel
Type: string
Read only with presence.

Not in this mode (typed never):

  • presence: Leave it out for no dot; pass it with presenceLabel to show one.

Native attributes

Also accepts the attributes in React's HTMLAttributes (id, aria-*, event handlers), passed to the rendered element, except those named on this page.

Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

  • author: Not a prop. The author is name: <Message role="user" name="Ana">.
  • content: Not a prop. The message is the children: <Message role="user" name="Ana">Hi</Message>.
  • error: Not a prop. A message that did not send is failed: <Message role="user" failed={{ onRetry: retry }}>.
  • from: Not a prop. Who wrote it is role: <Message role="user" name="Ana">.
  • onRetry: Not a prop. Retry is part of failed: <Message role="user" failed={{ onRetry: retry }}>.
  • own: Not a prop. The user's own messages are role="user": <Message role="user" variant="chat">.
  • replyTo: Not a prop. The message this one replies to is quote: <Message quote={{ author: "Ana", excerpt: "Lunch?" }}>.
  • sender: Not a prop. The author is name: <Message role="user" name="Ana">.
  • status: Not a prop. A message that did not send is failed: <Message role="user" failed={{ onRetry: retry }}>.
  • timestamp: Not a prop. The time is time, formatted: <Message time="9:41 AM" dateTime={iso}>.

className is for layout only

Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: Message merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.

Alignment classes (text-center) warn too: set alignment on a parent element.

Types

AvatarPresence

A person's presence, drawn as a dot with its own shape: a check (available), a solid dot (busy), a bar (do not disturb), a clock (away), a ring with a cross (offline) and a ring with an arrow (out of office), so it never relies on color alone.

type AvatarPresence = "available" | "busy" | "away" | "do-not-disturb" | "offline" | "out-of-office";

A menu entry for items: an on/off setting that stays in the menu when toggled.

interface MenuCheckboxEntry {
  type: "checkbox";
  label: string;
  checked?: boolean | undefined;
  defaultChecked?: boolean | undefined;
  onCheckedChange?: ((checked: boolean) => void) | undefined;
  shortcut?: MenuShortcut | undefined;
  disabled?: boolean | undefined;
  /** The id of the agent action it performs, set as `data-ax-action` on it, as on an item. */
  axAction?: string | undefined;
}

One entry in a Menu's items: an item (the default type), a checkbox, a radio group, a group, a submenu or a separator.

type MenuEntry =
  | MenuItemEntry
  | MenuCheckboxEntry
  | MenuRadioEntry
  | MenuGroupEntry
  | MenuSubmenuEntry
  | { type: "separator" };

A menu entry for items: entries under a label.

interface MenuGroupEntry {
  type: "group";
  label: string;
  items: readonly MenuEntry[];
}

A menu entry for items: an action, or a link with href.

interface MenuItemEntry {
  type?: "item" | undefined;
  /** The item's text. */
  label: string;
  /**
   * An icon before the text, as a `MenuItem` shows one in its children: an `<svg>` drawn at 16px
   * in the item's color. It is decoration: screen readers hear the label alone.
   */
  icon?: ReactNode;
  /** Runs when the item is chosen, by click, tap or Enter. The menu then closes. */
  onClick?: (() => void) | undefined;
  /** Makes the item a link, through `OuroProvider`'s router link for an internal path. */
  href?: string | undefined;
  /** The keys of the item's shortcut, shown at its end: `["mod", "e"]`. It does not bind them. */
  shortcut?: MenuShortcut | undefined;
  /** `"destructive"` for an action that deletes or cannot be undone. */
  tone?: "destructive" | undefined;
  /** Shown, but cannot be chosen. */
  disabled?: boolean | undefined;
  /**
   * The id of the agent action this item performs, set as `data-ax-action` on it, so the component
   * that declares the action finds its control: `axAction: "mute"`.
   */
  axAction?: string | undefined;
}

A menu entry for items: one choice of several, as radio items under an optional label.

interface MenuRadioEntry {
  type: "radio";
  /** Names the group: "Sort by". */
  label?: string | undefined;
  value?: string | undefined;
  defaultValue?: string | undefined;
  onValueChange?: ((value: string) => void) | undefined;
  options: ReadonlyArray<{ value: string; label: string; shortcut?: MenuShortcut | undefined; disabled?: boolean | undefined }>;
}

Keys for a shortcut, as Kbd takes them: ["mod", "e"] shows ⌘E on Apple platforms and Ctrl+E elsewhere.

type MenuShortcut = readonly string[] | (string & { 'Use an array: shortcut={["mod", "k"]}': never });

A menu entry for items: an item that opens a nested menu.

interface MenuSubmenuEntry {
  type: "submenu";
  label: string;
  items: readonly MenuEntry[];
  disabled?: boolean | undefined;
  /** The text of the item that goes back from the submenu in the phone sheet. Default "Back". */
  backLabel?: string | undefined;
}

MessageAction

One action in a message's action bar: an icon button with a tooltip.

interface MessageAction {
  /** Identifies the action among the message's actions ("reply"). */
  key: string;
  /** The button's name, which its tooltip shows too: "Reply". */
  label: string;
  /** What the button shows: an icon element (`<ReplyIcon />`) or an emoji. Screen readers hear `label` instead. */
  icon: ReactNode;
  /** Runs when the action is pressed. */
  onClick: (event: MouseEvent<HTMLElement>) => void;
  /** Shown, but cannot be pressed: an action the app has turned off for now. */
  disabled?: boolean | undefined;
  /**
   * The keys that run it, as `Kbd` takes them (`["1"]`), shown after its name in its tooltip and
   * under its emoji in the menu's row of reactions, and given to screen readers. It only shows the keys; bind them in your app.
   */
  shortcut?: MenuShortcut | undefined;
}

MessageActionsPlacement

Where a chat or reply message's action bar sits: corner over the bubble's top corner, raised over the line above it, or beside the bubble, past its end and level with its top.

type MessageActionsPlacement = "corner" | "beside";

MessageBubble

How a chat message's bubble, or a reply drawn as one, looks: filled, the user's own in the primary color and others' on the muted surface, round with a small corner toward the sender; or raised, others' on a raised card and the user's own in a tint of the primary color.

type MessageBubble = "filled" | "raised";

MessageFailed

What the person can do about a message of their own that did not send: Message's failed.

interface MessageFailed {
  /**
   * Sends the message again. Required, so a message that did not send always offers another try.
   */
  onRetry: () => void;
  /** Deletes the message that did not send. Leave it out to offer Retry alone. */
  onDelete?: (() => void) | undefined;
}

MessageNoPresenceProps

A Message without a presence dot.

interface MessageNoPresenceProps {
  /** Leave it out for no dot; pass it with `presenceLabel` to show one. */
  presence?: undefined;
  /** Read only with `presence`. */
  presenceLabel?: string | undefined;
}

MessageOwnProps

The props Message adds to the <article> attributes it passes through.

interface MessageOwnProps {
  /**
   * Who wrote it. `user` is the person using the app: their messages sit at the end, in
   * the primary color. `assistant` is anyone else, such as an agent, at the start. `system`
   * is the app's own note ("Ana joined"), centered and quiet.
   */
  role: MessageRole | (string & { 'Use "user", "assistant" or "system"': never });
  /**
   * How the message is laid out. `default` suits a conversation with an assistant. `chat`
   * suits people writing to each other: others' messages at the start under their name and
   * time, the user's own at the end with only the time, each bubble at most 85% of the row's
   * width and 36rem, so a line stays readable in a wide window. `chat` also suits a channel without threads, which reads as a chat.
   * `post` is a post in a threaded channel, unboxed: the author's name and time, an optional
   * `subject`, the text, then the `footer` and the `thread` under it, with room around it and
   * a hairline from the text's start to the end between it and a post just before it. `bubble`
   * sets how a `chat` message's bubble looks, and draws a `reply` as one. `reply`
   * is a reply in a thread: the same line of name and time, with the text unboxed under it,
   * starting where a post's text starts. In `post` and `reply` the user's own messages sit at
   * the start too.
   * @default "default"
   */
  variant?: MessageVariant | (string & { 'Use "default", "chat", "post" or "reply"': never }) | undefined;
  /**
   * How a `chat` message's bubble looks. `filled`, the default, is a messaging app's: the user's
   * own message filled as a primary button is (`--ouro-action`, else the primary color), others'
   * on the muted surface, each round with a smaller corner toward its sender, with an edge that
   * forced colors draw. Inside the user's own bubble, the theme's colors follow the fill, so a
   * link (underlined), a mention, a quote, a button or a focus ring in it stays readable on it; on others', a link or mention in the primary color is
   * a shade nearer the text color, so it keeps 4.5:1 on the muted surface. `raised` is a workplace chat's: others' messages on a raised card with
   * a hairline and a small shadow, the user's own in a tint of the primary color. A `reply` with a
   * `bubble` is laid out as a chat's message, for a thread that reads as a chat: the user's own at
   * the end, others' at the start under their name, each in a bubble of that look. A `reply`
   * without one stays unboxed, and `default` and `post` ignore it.
   * @default "filled" on a `chat` message; unboxed on a `reply`
   */
  bubble?: MessageBubble | (string & { 'Use "filled" or "raised"': never }) | undefined;
  /**
   * The author's name. Screen readers hear it before the message, and the avatar's
   * initials come from it. Required, so every message says who wrote it. The user's own
   * name is read but not shown.
   *
   * Pass `null` while the name is still loading: a name-sized Skeleton line takes the name's
   * place on its line, the avatar shimmers in its own shape, and screen readers hear
   * `nameLoadingLabel`. Nothing moves when the name arrives.
   */
  name: string | null;
  /** What screen readers hear while `name` is `null`, for other languages. @default "Name loading" */
  nameLoadingLabel?: string | undefined;
  /**
   * What shows as the author's name in place of `name`, when plain text cannot draw it: their
   * name with its emoji as pictures (`<EmojiText text={person.name} catalogue={catalogue} />`).
   * `name` stays what screen readers hear and the avatar's initials come from.
   */
  nameContent?: ReactNode;
  /**
   * A quiet tag after the author's name, such as "AI agent" for an agent or "Guest": small and
   * muted beside the name, not a badge, and read after it ("Halden, AI agent"). Where the name
   * is hidden (a `continued` message, the user's own `chat` messages) the tag is too, and both
   * are still read.
   */
  nameTag?: string | undefined;
  /**
   * The author's picture URL. Without it the avatar shows initials; `false` shows no avatar.
   * For the author's presence, pass `presence`. Pass an `Avatar` element instead for a square
   * avatar or a color of your own: `avatar={<Avatar name="Ana" src={url} alt="" size="sm" />}`
   * (`alt=""`, since the name is written beside it). In `chat`, the user's own messages show
   * no avatar.
   */
  avatar?: string | false | ReactElement | undefined;
  /**
   * When it was written, already formatted for the reader ("9:41 AM"), shown after the name.
   * Format it in the browser after hydration, so the server and the reader's clock agree.
   */
  time?: string | undefined;
  /** The same moment for machines, as an ISO date (`2026-05-01T09:41`): the `<time>` element's `dateTime`. */
  dateTime?: string | undefined;
  /**
   * The message follows one from the same author: no avatar and no name line, so a run of
   * messages reads as one. Its time shows beside it on hover and focus. Screen readers still
   * hear the name and time. Not for `post`: every post shows its author.
   */
  continued?: boolean | undefined;
  /** A post's subject, in semibold above its text. `post` only. */
  subject?: ReactNode;
  /** The message: text, or rich content such as a list or a code block. */
  children?: ReactNode;
  /**
   * The message this one replies to, at the top of the bubble or post, above the text: a bar,
   * the author and a line of what they wrote, as Composer's `quote` shows it before the reply
   * is sent. Screen readers hear it first, as "Quoting Ana: …". With `onActivate` it is a
   * button, to show the quoted message.
   */
  quote?: MessageQuote | undefined;
  /**
   * Files and pictures sent with the message, shown under it: `<Attachment>` elements, and
   * pictures as `<Lightbox layout="mosaic">`, which keeps them to a chat's size and opens them
   * full size.
   */
  attachments?: ReactNode;
  /** Under the message: its reactions (`<ReactionBar>`), "Edited", whether it was delivered or seen. */
  footer?: ReactNode;
  /** Under a post, or a message in a channel without threads: its thread, as a `<ThreadSummary>`. */
  thread?: ReactNode;
  /**
   * The message's actions, as icon buttons with tooltips in a small bar at the bubble's top end
   * (`actionsPlacement`) or over a post's top end. The bar shows while the pointer is over
   * the message, while focus is in it, and after a tap on a touch screen. It is a toolbar: one Tab stop, with the arrow
   * keys moving between its buttons. In a `MessageList` the list draws one bar for all its
   * messages, on the message the pointer is over (or that keyboard focus is in, or was tapped), and
   * hides it as the pointer leaves, the list scrolls under it, the window loses focus or the menu opens,
   * so at most one bar shows and none stays behind. Escape hides it.
   */
  actions?: readonly MessageAction[] | undefined;
  /**
   * Where the action bar sits on a `chat` or `reply` message. `beside` puts it just past the
   * bubble's end, level with its top, so it covers neither the name nor the message and stays
   * with the bubble however wide the row; where the row has no room left, it ends at the row's
   * edge, over the bubble's corner. On the user's own `chat` message it sits before the bubble.
   * `corner` puts it over the bubble's top corner, raised over the line above. A `post`'s bar is
   * over its top end, and the default layout's over its bubble's corner.
   *
   * @default "beside" on a `reply`, "corner" on a `chat`
   */
  actionsPlacement?: MessageActionsPlacement | (string & { 'Use "corner" or "beside"': never }) | undefined;
  /**
   * More actions, in a menu that opens from the last button in the action bar: the same
   * entries as Menu's `items`, each with an optional `icon` before its label and its `shortcut` at its end. The bar stays while the menu is open. The bar renders in the
   * browser once its code loads, so server HTML has the message without it; Menu's code loads as
   * soon as a bar with a menu is on the page, so the first press opens it without a wait. In a `MessageList`, the list has one menu for all its
   * messages: the bar's menu button, a right-click on the message (except on a field, a link, a picture or
   * selected text, which keep the browser's menu) and Shift+F10 or the context-menu key on it all open it, named by `actionsMenuLabel`, and
   * closing it puts focus back on the message.
   */
  menu?: readonly MenuEntry[] | undefined;
  /**
   * Reactions at the top of the message's menu, as a row of emoji: each an action whose `icon` is the
   * emoji and whose `label` names it ("Like"). Left and right move along the row. Put one that opens
   * a picker of every reaction last ("More reactions").
   */
  quickReactions?: readonly MessageAction[] | undefined;
  /** What screen readers hear for the row of reactions in the menu, for other languages. @default "Reactions" */
  quickReactionsLabel?: string | undefined;
  /** What screen readers hear for the action bar, for other languages. @default "Message actions" */
  actionsLabel?: string | undefined;
  /** What screen readers hear for the menu a `MessageList` opens for the message. @default `Message actions for ${name}` */
  actionsMenuLabel?: string | undefined;
  /** The menu button's name, for other languages. @default "More actions" */
  menuLabel?: string | undefined;
  /**
   * The reply is still being written. The message is `aria-busy`, an empty one shows a
   * typing indicator, and `MessageList` announces it once, when streaming ends.
   */
  streaming?: boolean | undefined;
  /** What screen readers hear for the typing indicator, for other languages. @default "Writing" */
  streamingLabel?: string | undefined;
  /**
   * The message did not send, and what the person can do about it: `{ onRetry, onDelete }`. A
   * calm marker under it says "Not sent" beside Retry and Delete buttons, and stays until the app
   * clears `failed`, so a failed send never goes quiet. Screen readers hear the marker as the
   * message's description. It appears at once, with no motion. To put the text back in a
   * `Composer` instead, set the Composer's `value` to it and delete the message.
   */
  failed?: MessageFailed | false | undefined;
  /** What the marker under a `failed` message says, for other languages. @default "Not sent" */
  failedLabel?: string | undefined;
  /** The name of a `failed` message's retry button, for other languages. @default "Retry" */
  retryLabel?: string | undefined;
  /** The name of a `failed` message's delete button, for other languages. @default "Delete" */
  deleteLabel?: string | undefined;
  /** Layout only: margins (`mt-4`). Appearance classes warn in development; the colors follow `role`. */
  className?: string | undefined;
  ref?: Ref<HTMLElement> | undefined;

  /** Not a prop. Who wrote it is `role`: `<Message role="user" name="Ana">`. */
  from?: { 'Use <Message role="user" name="Ana">': never };
  /** Not a prop. The author is `name`: `<Message role="user" name="Ana">`. */
  sender?: { 'Use <Message role="user" name="Ana">': never };
  /** Not a prop. The author is `name`: `<Message role="user" name="Ana">`. */
  author?: { 'Use <Message role="user" name="Ana">': never };
  /** Not a prop. The message is the children: `<Message role="user" name="Ana">Hi</Message>`. */
  content?: { 'Use <Message role="user" name="Ana">Hi</Message>': never };
  /** Not a prop. The user's own messages are `role="user"`: `<Message role="user" variant="chat">`. */
  own?: { 'Use <Message role="user" variant="chat">': never };
  /** Not a prop. The time is `time`, formatted: `<Message time="9:41 AM" dateTime={iso}>`. */
  timestamp?: { 'Use <Message time="9:41 AM" dateTime={iso}>': never };
  /** Not a prop. The message this one replies to is `quote`: `<Message quote={{ author: "Ana", excerpt: "Lunch?" }}>`. */
  replyTo?: { 'Use <Message quote={{ author: "Ana", excerpt: "Lunch?" }}>': never };
  /** Not a prop. A message that did not send is `failed`: `<Message role="user" failed={{ onRetry: retry }}>`. */
  error?: { 'Use <Message role="user" failed={{ onRetry: retry }}>': never };
  /** Not a prop. A message that did not send is `failed`: `<Message role="user" failed={{ onRetry: retry }}>`. */
  status?: { 'Use <Message role="user" failed={{ onRetry: retry }}>': never };
  /** Not a prop. Retry is part of `failed`: `<Message role="user" failed={{ onRetry: retry }}>`. */
  onRetry?: { 'Use <Message role="user" failed={{ onRetry: retry }}>': never };
}

MessagePresenceProps

A Message whose avatar shows the author's presence, and the words it says.

interface MessagePresenceProps {
  /**
   * The author's presence, as Avatar's dot on the avatar's lower end corner: available,
   * busy, away, do not disturb, offline or out of office, each in its own shape and tone. It
   * shows wherever the avatar does, so not on a `continued` message or the user's own `chat`
   * messages. With an `Avatar` element as `avatar`, pass `presence` to that Avatar instead.
   */
  presence: AvatarPresence | (string & { 'dnd is "do-not-disturb"; ooo is "out-of-office"': never });
  /**
   * What the dot says, such as "Busy" or "In a meeting". Screen readers hear it on the avatar,
   * as Avatar reads a presence beside a written name. Required with `presence`, so no dot is
   * silent.
   */
  presenceLabel: string;
}

MessageProps

Props for Message: its own props, presence with presenceLabel or neither, and the <article> attributes it passes through.

type MessageProps = Omit<HTMLAttributes<HTMLElement>, keyof MessageOwnProps | "content" | "presence" | "presenceLabel"> &
  MessageOwnProps &
  (MessagePresenceProps | MessageNoPresenceProps);

MessageQuote

The message a message replies to, for Message's quote: who wrote it and a line of what they wrote.

interface MessageQuote {
  /** Who wrote the quoted message: "Ana Lind", or "You". */
  author: string;
  /**
   * What they wrote, or its start. One line shows, cut with an ellipsis.
   */
  excerpt: string;
  /** The author's picture, before the name: an `Avatar` element, `<Avatar name="Ana" src={url} alt="" size="xs" />`. */
  avatar?: ReactElement | undefined;
  /**
   * Makes the quote a button that shows the quoted message, as by scrolling to it. In a `MessageList`, which may not
   * have drawn a message far up a long conversation, call its `actionsRef.current.reveal(id, { focus: true })`.
   */
  onActivate?: (() => void) | undefined;
  /** What screen readers hear in place of the author's name, before the excerpt. @default `Quoting ${author}` */
  label?: string | undefined;
}

MessageRole

Who wrote a message: the person using the app, the assistant, or the app itself.

type MessageRole = "user" | "assistant" | "system";

MessageVariant

How a message is laid out: default for a conversation with an assistant, chat for a chat between people, post for a post in a channel, and reply for a reply in its thread.

type MessageVariant = "default" | "chat" | "post" | "reply";

Claim sources