Component reference
ChatMessage
One message in a conversation: the author, the time, the body and its actions.
Preview
Text, files and evidence
The message owns identity and arrangement; the Bubble owns the reading surface. Attachments and process can sit outside the bubble, and actions only echo local state.
render: () => (
<Measure>
<Stack>
<ChatFrame>
<ChatMessageList ariaLabel="Message composition example">
<ChatMessage
role="user"
author="You"
timestamp="14:20"
surface="none"
footer={<span>Delivered</span>}
>
<ChatAttachments
items={[
{
id: "log",
name: "search-request.log",
url: "data:text/plain,Search%20request%20example",
mediaType: "text/plain",
sizeBytes: 22,
},
]}
/>
<ChatBubble>
<p>
This is the log from continuous searches. Help me understand why an old result
overwrote a new one; give me a suggestion first.
</p>
</ChatBubble>
</ChatMessage>
<ChatMessage
role="assistant"
author="Assistant"
surface="none"
header={
<>
<strong>Assistant</strong>
<span>Investigation record · Static example</span>
</>
}
footer={<span>No real request ran; the suggestion waits for your confirmation.</span>}
actions={
<ChatMessageActions>
<Button
variant="outline"
size="small"
data-message-choice="Kept the fix suggestion"
>
Keep suggestion
</Button>
<Button
variant="quiet"
size="small"
data-message-choice="Marked for further checking"
>
Keep checking
</Button>
</ChatMessageActions>
}
>
<ChatBubble>
<p>
<strong>
The old request returned later, and the write did not confirm that it was still
the current request.
</strong>
Keep the interface and check the request identity before submitting results.
</p>
</ChatBubble>
<ChatProcess label="View investigation evidence · 2 items">
<ChatToolCall
icon={<Icon name="file-text" />}
label="Read request log"
state="complete"
collapsible
detail="In this example the second request finished first."
>
<p>
14:20:01 searched for “components”; 14:20:02 searched for “component library”.
</p>
</ChatToolCall>
<ChatToolCall
icon={<Icon name="file-text" />}
label="Check the result write path"
state="complete"
collapsible
detail="The callback replaces the list directly, without an identity check."
>
<p>Fix location: the result submission callback.</p>
</ChatToolCall>
</ChatProcess>
<ChatBubble variant="plain">
<p>
Canceling the old request can reduce work, but a final check before writing is
still needed:
</p>
<pre tabIndex={0} aria-label="Suggested code">
<code>{`if (currentRequest === request) {
renderResults(results);
}`}</code>
</pre>
</ChatBubble>
<ChatSources items={[reference]} />
</ChatMessage>
<ChatMessage
role="user"
author="You"
status="error"
surface="none"
footer={<span>Send failed; the draft is still local.</span>}
actions={
<ChatMessageActions>
<Button
variant="quiet"
size="small"
data-message-choice="Kept the retry content (local example)"
>
Retry send
</Button>
</ChatMessageActions>
}
>
<ChatBubble variant="filled">
<p>Apply this fix, and add a test for out-of-order responses while you're at it.</p>
</ChatBubble>
</ChatMessage>
</ChatMessageList>
</ChatFrame>
<Note role="status" data-message-result>
Actions only report back to the local example.
</Note>
</Stack>
</Measure>
),
setup({ root, onCleanup }) {
const click = (event: Event) => {
const button = (event.target as Element).closest<HTMLElement>("[data-message-choice]");
if (button)
root.querySelector("[data-message-result]")!.textContent = button.dataset.messageChoice!;
};
root.addEventListener("click", click);
onCleanup(() => root.removeEventListener("click", click));
},
});
export const ChatProcessDetailStory = defineStory({
id: "chat-process-detail",
title: "Process · Summary and execution detail",
group: "Conversation components",
groupOrder: 60,
component: "ChatProcess",
variant: "Collapse and expand",
description:
"The process summary keeps its position and execution details expand on demand; expansion and collapse come from native details.",
render: () => (
<Measure>
<Stack>
<ChatProcess label="Check request order · Completed">
<p>Queries are sent in input order, but responses return in network completion order.</p>
<p>Submit only the current request's results to avoid overwrites from old responses.</p>
</ChatProcess>
<ChatProcess label="Waiting for verification · 1 item" open>
<ChatToolCall
icon={<Icon name="search" />}
label="Verify quick successive input and list recovery after clearing the search term"
state="waiting"
detail="Requires a connection to the real service."
/>
</ChatProcess>
</Stack>
</Measure>
)Installation
bun add meowtail
Usage
Taken from the examples on this page.
import { ChatMessage } from "meowtail";
<ChatMessage
role="assistant"
author="Assistant handling the component review"
continuation
timestamp="09:42"
avatar={<AssistantMark />}
>
Continues the previous reply in the same column: the avatar slot stays but goes blank,
and the author line keeps the turn attributed.
</ChatMessage>Examples
The other stories that show this component.
Avatar, long title, and actions
The avatar, author, time, body, and actions of one conversation; long content must not crowd out actions or break narrow screens.
render: () => (
<div className="workshop-composition">
<ChatFrame>
<ChatHeader
eyebrow="Current project"
title="Review the component's text, icons, dark surfaces, and narrow-screen interactions"
description="The title and description wrap when space runs out without hiding the work context."
status={<ChatStatus label="Waiting for review" tone="waiting" />}
actions={
<IconButton label="View conversation information" data-message-info>
<Icon name="eye" />
</IconButton>
}
/>
<ChatMessageList ariaLabel="Component review sample messages">
<ChatMessage
role="assistant"
author="Assistant handling the component review"
timestamp="09:41"
avatar={<AssistantMark />}
actions={
<ChatMessageActions>
<Button variant="quiet" size="small" data-message-action="Marked this reply.">
Mark reply
</Button>
<IconButton label="Reply details" data-message-info>
<Icon name="eye" />
</IconButton>
<Button variant="quiet" size="small" disabled>
Waiting for check results
</Button>
</ChatMessageActions>
}
>
<p>
The title, author, and actions must keep the reading order together. Longer paths
should also stay inside the current container:
</p>
<p>
packages/meowtail/stories/review-of-typography-and-component-interaction-states.tsx
</p>
</ChatMessage>
<ChatMessage
role="assistant"
author="Assistant handling the component review"
continuation
timestamp="09:42"
avatar={<AssistantMark />}
>
Continues the previous reply in the same column: the avatar slot stays but goes blank,
and the author line keeps the turn attributed.
</ChatMessage>
<ChatMessage
role="user"
author="You"
timestamp="09:43"
avatar={<Icon name="user-round" />}
>
Keep the relationship between input, messages, and attachments, and make sure long
content stays fully readable.
</ChatMessage>
<ChatMessage
role="assistant"
author="Assistant"
timestamp="09:44"
actions={
<ChatMessageActions>
<Button data-message-action="Opened the local suggestion sample.">
Review suggestion
</Button>
<Button variant="outline" data-message-action="Opened the local diff sample.">
View diff
</Button>
<button
type="button"
data-message-action="Kept the check record; you can come back later"
>
Handle later
</button>
</ChatMessageActions>
}
>
<p>
The check record is ready. You can review the suggestion first and then decide whether
to adopt it, or keep the record and handle it later.
</p>
</ChatMessage>
</ChatMessageList>
</ChatFrame>
<ChatHeader title="Compact conversation with a title only" />
<Note role="status" data-message-feedback>
Actions only report back to the local sample.
</Note>
</div>
)Roles and feedback states
Fixed-state examples: online, waiting, error and generating. There is no background task or model call.
render: () => (
<div className="workshop-composition">
<div className="workshop-row">
<ChatStatus label="Ready" tone="online" />
<ChatStatus label="In progress" tone="busy" />
<ChatStatus label="Waiting for confirmation" tone="waiting" />
<ChatStatus label="Connection lost" tone="error" />
<ChatStatus label="Not started" tone="idle" />
</div>
<ChatFrame>
<ChatMessageList>
<ChatMessage role="system" author="System" timestamp="09:41">
Connected to the current project.
</ChatMessage>
<ChatMessage role="user" author="You" status="pending">
Check how the components behave on a narrow screen. (Waiting to send)
</ChatMessage>
<ChatMessage role="assistant" author="Assistant" status="streaming">
Checking the layout of buttons, inputs and dialogs…
</ChatMessage>
<ChatMessage role="tool" author="Browser check">
Opened the 390px canvas and kept the original content.
</ChatMessage>
<ChatMessage role="assistant" author="Assistant" status="error">
The connection was lost and the check did not finish. Input is still kept.
</ChatMessage>
<ChatMessage role="assistant" author="Assistant" continuation>
Checking can continue once the connection returns.
</ChatMessage>
</ChatMessageList>
</ChatFrame>
</div>
)API reference
Props
| Prop | Type | Default |
|---|---|---|
roleRequired | ChatMessageRole | – |
authorRequired | string | – |
timestamp | string | – |
dateTime | string | – |
status | ChatMessageStatus | "complete" |
avatar | Child | – |
actions | Child | – |
header | Child | – |
footer | Child | – |
surfaceOmit the automatic surface when composing multiple bubbles, files or process blocks. | "auto" | "none" | "auto" |
continuationJoins a reply, retaining its author and the column of a supplied avatar. | boolean | false |
Also takes className, children and the attributes and events of its root element.
DOM parts
rootavatarbodymetaauthortimestatuscontentactionsheaderfooter
Registry
The component is registered as chat-message in the meowtail component registry.