Message
freeMessage lays out one row of a conversation with an optional avatar and a role-aware content column; compound parts add reactions, a hover-reveal action row, a typing indicator, tight grouping, and open marker rows for status notes or separators.
import Message from '@/components/composites/Message';
import Avatar from '@/components/ui/Avatar';
export default function UsageDemo() {
return (
<div className="flex flex-col gap-6">
<Message
align="start"
avatar={
<Avatar shape="round"
size={32}
src="/img/avatars/thumb-1.jpg"
alt="Assistant"
/>
}
>
<Message.Content variant="subtle">
Here is a quick summary of the latest deployment. Three
services updated cleanly and no errors were reported.
</Message.Content>
<Message.Meta>10:14 AM</Message.Meta>
</Message>
<Message
align="end"
avatar={
<Avatar shape="round" size={32} src="/img/avatars/thumb-2.jpg" alt="You" />
}
>
<Message.Content variant="solid">
Thanks, can you also check the staging environment?
</Message.Content>
<Message.Meta>10:15 AM</Message.Meta>
</Message>
</div>
);
}Installation
Add this component with the NateUI CLI.
npx nateui@latest add MessageExamples
Variant
import Message from '@/components/composites/Message';
export default function VariantDemo() {
return (
<div className="flex flex-col gap-4 w-full">
<Message align="start">
<Message.Content variant="solid">
Solid gets a strong primary fill.
</Message.Content>
</Message>
<Message align="start">
<Message.Content variant="subtle">
Subtle gets a soft secondary fill.
</Message.Content>
</Message>
<Message align="start">
<Message.Content variant="default">
Default gets a bordered neutral surface.
</Message.Content>
</Message>
<Message align="start">
<Message.Content variant="ghost">
Ghost sits directly on the page with no surface.
</Message.Content>
</Message>
</div>
);
}Alignment
import Message from '@/components/composites/Message';
export default function AlignmentDemo() {
return (
<div className="flex flex-col gap-4 w-full">
<Message align="start">
<Message.Content variant="solid">Start-aligned row.</Message.Content>
</Message>
<Message align="end">
<Message.Content variant="solid">End-aligned row.</Message.Content>
</Message>
</div>
);
}Actions
Message.Actions lays out a row of caller-supplied controls. revealOnHover hides the row until the message is hovered or a control inside it receives focus, so keyboard users can still reach it.
import { useState } from 'react';
import Message from '@/components/composites/Message';
import Button from '@/components/ui/Button';
import Tooltip from '@/components/ui/Tooltip';
import { PiCopy, PiThumbsDown, PiThumbsUp } from 'react-icons/pi'
export default function ActionsDemo() {
const [copied, setCopied] = useState(false);
return (
<Message align="start">
<Message.Content variant="ghost">
Hover this message, or tab into it, to reveal the action row.
</Message.Content>
<Message.Actions revealOnHover>
<Tooltip title={copied ? 'Copied' : 'Copy'}>
<Button
type="button"
variant="ghost"
shape="circle"
size="sm"
icon={<PiCopy />}
aria-label="Copy message"
onClick={() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}}
/>
</Tooltip>
<Tooltip title="Good response">
<Button
type="button"
variant="ghost"
shape="circle"
size="sm"
icon={<PiThumbsUp />}
aria-label="Good response"
/>
</Tooltip>
<Tooltip title="Bad response">
<Button
type="button"
variant="ghost"
shape="circle"
size="sm"
icon={<PiThumbsDown />}
aria-label="Bad response"
/>
</Tooltip>
</Message.Actions>
</Message>
);
}Reactions
Message.Reactions overlaps the bottom corner of Message.Content — render it as a child of Message.Content, not a sibling. align picks which corner it hangs off, matching the row's own align. Leave extra gap between stacked messages so the overlap has room.
import Message from '@/components/composites/Message';
export default function ReactionsDemo() {
return (
<div className="flex flex-col gap-8 w-full">
<Message align="start">
<Message.Content variant="subtle">
Deploying to prod real quick.
</Message.Content>
</Message>
<Message align="start">
<Message.Content variant="subtle">
It's always a one-line change.
<Message.Reactions align="start" aria-label="Reactions: thumbs up">
<span>👍</span>
</Message.Reactions>
</Message.Content>
</Message>
<Message align="end">
<Message.Content variant="solid">
Alright, let me take a look.
<Message.Reactions align="end" aria-label="Reactions: eyes, rocket">
<span>👀</span>
<span>🚀</span>
</Message.Reactions>
</Message.Content>
</Message>
</div>
);
}Type Indicator
import Message from '@/components/composites/Message';
import Avatar from '@/components/ui/Avatar';
export default function TypeIndicatorDemo() {
return (
<div className="flex flex-col gap-6 w-full">
<Message
align="end"
avatar={
<Avatar shape="round" size={32} src="/img/avatars/thumb-2.jpg" alt="You" />
}
>
<Message.Content variant="solid">
Thanks, can you also check the staging environment?
</Message.Content>
<Message.Meta>10:15 AM</Message.Meta>
</Message>
<Message
align="start"
avatar={
<Avatar shape="round"
size={32}
src="/img/avatars/thumb-1.jpg"
alt="Assistant"
/>
}
>
<Message.Content variant="ghost">
<Message.TypeIndicator />
</Message.Content>
</Message>
</div>
);
}Group
Message.Group stacks consecutive Message rows from the same sender with a tighter vertical rhythm than separate turns get.
import Message from '@/components/composites/Message';
import Avatar from '@/components/ui/Avatar';
export default function GroupDemo() {
return (
<Message.Group>
<Message align="start" avatar={<div className="size-8" />}>
<Message.Content variant="subtle">
Found the issue — the retry loop was swallowing the timeout error.
</Message.Content>
</Message>
<Message
align="start"
avatar={
<Avatar shape="round"
size={32}
src="/img/avatars/thumb-1.jpg"
alt="Assistant"
/>
}
>
<Message.Content variant="subtle">
Pushed a fix, re-running the job now.
</Message.Content>
<Message.Meta>10:16 AM</Message.Meta>
</Message>
</Message.Group>
);
}Marker
Message.Marker is an open row slot for date separators, system events, or live status notes. Pass separator to add flanking hairlines, and add any live-region role yourself when the marker represents changing status.
import Message from '@/components/composites/Message';
import Avatar from '@/components/ui/Avatar';
import Tag from '@/components/ui/Tag';
export default function MarkerDemo() {
return (
<div className="flex flex-col gap-6 w-full">
<Message.Marker>
<Tag className="bg-card">Jun 12, 2026</Tag>
</Message.Marker>
<Message
align="start"
avatar={
<Avatar shape="round"
size={32}
src="/img/avatars/thumb-1.jpg"
alt="Assistant"
/>
}
>
<Message.Content variant="subtle">
Found the issue — the retry loop was swallowing the timeout error.
</Message.Content>
<Message.Meta>4:32 PM</Message.Meta>
</Message>
<Message
align="start"
avatar={
<Avatar shape="round"
size={32}
src="/img/avatars/thumb-1.jpg"
alt="Assistant"
/>
}
>
<Message.Content variant="subtle">
Pushed a fix, re-running the job now.
</Message.Content>
<Message.Meta>4:35 PM</Message.Meta>
</Message>
<Message.Marker separator>Today</Message.Marker>
<Message
align="start"
avatar={
<Avatar shape="round"
size={32}
src="/img/avatars/thumb-1.jpg"
alt="Assistant"
/>
}
>
<Message.Content variant="subtle">
Here is a quick summary of the latest deployment. Three services
updated cleanly and no errors were reported.
</Message.Content>
<Message.Meta>10:14 AM</Message.Meta>
</Message>
<Message
align="end"
avatar={
<Avatar shape="round" size={32} src="/img/avatars/thumb-2.jpg" alt="You" />
}
>
<Message.Content variant="solid">
Thanks, can you also check the staging environment?
</Message.Content>
<Message.Meta>10:15 AM</Message.Meta>
</Message>
</div>
);
}API
Message
| Prop | Description | Type | Default |
|---|---|---|---|
align | Which side of the conversation the row belongs to. | MessageAlign | 'start' |
avatar | Optional avatar node rendered at the row's outer edge and anchored to the lower edge of the direct Message.Content surface. Message.Meta and Message.Actions flow below without changing that anchor. | ReactNode | — |
children | Row content, typically Message.Content and Message.Meta. | ReactNode | — |
...divProps | Standard <div> attributes forwarded to the row. | ComponentProps<'div'> | — |
Message.Content
| Prop | Description | Type | Default |
|---|---|---|---|
variant | 'solid' for a strong primary fill, 'subtle' for a soft secondary fill, 'default' for a bordered neutral surface, 'ghost' for bare text with no surface. Matches Button's variant vocabulary. | MessageContentVariant | 'default' |
...divProps | Standard <div> attributes forwarded to the content block. | ComponentProps<'div'> | — |
Message.Meta
| Prop | Description | Type | Default |
|---|---|---|---|
...divProps | Standard <div> attributes forwarded to the meta line. With an avatar, the meta line flows below the message surface without changing the avatar anchor. | ComponentProps<'div'> | — |
Message.Reactions
Overlaps the bottom (or top) corner of Message.Content — render it as a child of Message.Content, which is position: relative for exactly this. Defaults role="img" so a screen reader announces the whole row once from a single aria-label, rather than reading each emoji individually — pass a descriptive aria-label summarizing the reactions.
| Prop | Description | Type | Default |
|---|---|---|---|
align | Which corner of the content block the reactions hang off. Match the row's own align. | MessageReactionsAlign | 'start' |
side | Which edge of the content block the reactions anchor to. | MessageReactionsSide | 'bottom' |
...divProps | Standard <div> attributes forwarded to the reactions row. | ComponentProps<'div'> | — |
Message.TypeIndicator
| Prop | Description | Type | Default |
|---|---|---|---|
label | Text announced to screen readers via a visually-hidden span. | string | 'Assistant is typing' |
...divProps | Standard <div> attributes forwarded to the root. | ComponentProps<'div'> | — |
Message.Group
Stacks consecutive Message rows from the same sender. Avatar-collapse (keeping text columns flush across the group) is a caller convention — pass an empty same-sized placeholder as avatar on every message but the last, not a prop on Message.Group itself. The final avatar anchors to the visible message surface; metadata remains below it.
| Prop | Description | Type | Default |
|---|---|---|---|
children | Consecutive Message rows from the same sender. | ReactNode | — |
...divProps | Standard <div> attributes forwarded to the group root. | ComponentProps<'div'> | — |
Message.Marker
| Prop | Description | Type | Default |
|---|---|---|---|
separator | Adds a hairline on either side of the marker's children. | boolean | false |
...divProps | Standard <div> attributes forwarded to the marker row. | ComponentProps<'div'> | — |
Message.Actions
| Prop | Description | Type | Default |
|---|---|---|---|
revealOnHover | Hides the action row until the message is hovered or focused within. | boolean | false |
children | Action controls, typically icon buttons. | ReactNode | — |
...divProps | Standard <div> attributes forwarded to the action row. | ComponentProps<'div'> | — |
Types
type MessageAlign = 'start' | 'end'
type MessageContentVariant = 'default' | 'subtle' | 'solid' | 'ghost'
type MessageReactionsAlign = 'start' | 'end'
type MessageReactionsSide = 'top' | 'bottom'
type MessageMarkerProps = ComponentProps<'div'> & { separator?: boolean }