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.
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-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10Then 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.10Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/messageIts 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.10import { 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
Alertinstead for a notice that is not part of a conversation, andtoast()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 aMessageListthe 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
MessageListopens for the message. actionsPlacement- Type:
"corner" | "beside" - Default:
"beside"on areply, "corner" on achat - Where the action bar sits on a
chatorreplymessage.besideputs 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 ownchatmessage it sits before the bubble.cornerputs it over the bubble's top corner, raised over the line above. Apost'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;
falseshows no avatar. For the author's presence, passpresence. Pass anAvatarelement 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). Inchat, the user's own messages show no avatar. bubble- Type:
"filled" | "raised" - Default:
"filled"on achatmessage; unboxed on areply - How a
chatmessage'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.raisedis 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. Areplywith abubbleis 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. Areplywithout one stays unboxed, anddefaultandpostignore 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 followrole. 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'sdateTime. deleteLabel- Type:
string - Default:
"Delete" - The name of a
failedmessage'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 clearsfailed, 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 aComposerinstead, set the Composer'svalueto it and delete the message. failedLabel- Type:
string - Default:
"Not sent" - What the marker under a
failedmessage 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 optionaliconbefore its label and itsshortcutat 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 aMessageList, 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 byactionsMenuLabel, 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
nullwhile 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 hearnameLoadingLabel. 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} />).namestays what screen readers hear and the avatar's initials come from. nameLoadingLabel- Type:
string - Default:
"Name loading" - What screen readers hear while
nameisnull, 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
continuedmessage, the user's ownchatmessages) 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
iconis the emoji and whoselabelnames 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
quoteshows it before the reply is sent. Screen readers hear it first, as "Quoting Ana: …". WithonActivateit 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
failedmessage's retry button, for other languages. role(required)- Type:
"user" | "assistant" | "system" - Who wrote it.
useris the person using the app: their messages sit at the end, in the primary color.assistantis anyone else, such as an agent, at the start.systemis 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, andMessageListannounces 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.
postonly. 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.
defaultsuits a conversation with an assistant.chatsuits 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.chatalso suits a channel without threads, which reads as a chat.postis a post in a threaded channel, unboxed: the author's name and time, an optionalsubject, the text, then thefooterand thethreadunder it, with room around it and a hairline from the text's start to the end between it and a post just before it.bubblesets how achatmessage's bubble looks, and draws areplyas one.replyis 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. Inpostandreplythe 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
continuedmessage or the user's ownchatmessages. With anAvatarelement asavatar, passpresenceto 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 withpresenceLabelto 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 isname:<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 isfailed:<Message role="user" failed={{ onRetry: retry }}>.from: Not a prop. Who wrote it isrole:<Message role="user" name="Ana">.onRetry: Not a prop. Retry is part offailed:<Message role="user" failed={{ onRetry: retry }}>.own: Not a prop. The user's own messages arerole="user":<Message role="user" variant="chat">.replyTo: Not a prop. The message this one replies to isquote:<Message quote={{ author: "Ana", excerpt: "Lunch?" }}>.sender: Not a prop. The author isname:<Message role="user" name="Ana">.status: Not a prop. A message that did not send isfailed:<Message role="user" failed={{ onRetry: retry }}>.timestamp: Not a prop. The time istime, 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";MenuCheckboxEntry
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;
}MenuEntry
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" };MenuGroupEntry
A menu entry for items: entries under a label.
interface MenuGroupEntry {
type: "group";
label: string;
items: readonly MenuEntry[];
}MenuItemEntry
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;
}MenuRadioEntry
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 }>;
}MenuShortcut
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 });MenuSubmenuEntry
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
- layout-classes: test:registry/ouro/components/ouro/message/message.test.tsx::Message > forwards a ref and lets app layout classes win
verify:testExists(registry/ouro/components/ouro/message/message.test.tsx::Message > forwards a ref and lets app layout classes win)
