Meowland docs
Workbench

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.

packages/meowtail/stories/chat-message.stories.tsxline 194tsx
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.

usage.tsxtsx
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.

packages/meowtail/stories/chat.stories.tsxline 38tsx
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.

packages/meowtail/stories/language.stories.tsxline 582tsx
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

PropTypeDefault
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.
booleanfalse

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.