# Chat Message

> One turn in a conversation with an agent: user bubble on the right, agent reply as rich text on the left.

Component · AI · Pro kit · [Live page](https://ui.twentyfirst.media/pro/components/ChatMessage/)

One turn in a conversation with an agent: user bubble on the right, agent reply as rich text on the left.

Props: `role` ("user" | "assistant" | "system"), `name`, `time`, `model`, `text` (Markdown subset: paragraphs, **bold**, `code`, - lists, 1. lists, ### headings, links, ``` code blocks), `streaming` (blinking caret, no actions yet), `tint` (agent color), `sources` ({title, icon, url | onClick}), `attachments` ({name, size}), `usage` (tokens · cost text), `onRetry`, `onFeedback("up" | "down")`, `actions` (false hides the row), `before` / `after` (slots for tool calls, reasoning, approvals).

- User messages are ink pills, agent replies are plain text on the surface: the agent's words get the space.
- Actions (copy, regenerate, thumbs) appear once streaming ends.
- Number sources in the order they were used; keep titles short.
- `RichText` and `CodeBlock` are exported on their own for docs, notes and logs.

## Example

```tsx
import { ChatMessage } from "@twentyfirstmedia/pro";

const md =
  'Three invoices are overdue, **€2,520** in total:\n\n- **#2041** Harbor & Co · €860\n- **#2058** Pinegate · €1,240\n- **#2063** Orbit Foods · €420\n\nThe fix for the reminder job:\n\n```ts\nconst due = invoices.filter(i => i.dueAt < now && !i.paid);\nawait sendReminders(due, { tone: "friendly" });\n```';
export default function Preview() {
  return (
    <div
      className="tf-panel"
      style={{
        gap: 24,
      }}
    >
      <ChatMessage
        role="user"
        name="Mara Ellis"
        text="Which invoices are overdue? Fix the reminder job too."
        attachments={[
          {
            name: "reminders.ts",
            size: "4 KB",
          },
        ]}
      />
      <ChatMessage
        role="assistant"
        name="Ledger"
        tint="butter"
        time="09:41"
        model="Claude Sonnet 5.5"
        text={md}
        sources={[
          {
            title: "Invoices · due",
            icon: "card",
          },
          {
            title: "jobs/reminders.ts",
            icon: "code",
          },
        ]}
        usage="1,842 tokens · €0.01"
        onRetry={() => {}}
      />
      <ChatMessage role="system" text="Ledger switched to Claude Haiku 5.5" />
      <ChatMessage
        role="assistant"
        name="Ledger"
        tint="butter"
        streaming
        text={"Sending reminders now. Harbor & Co first because"}
      />
    </div>
  );
}
```

## Props

### ChatMessage

| Prop | Type | Notes |
| --- | --- | --- |
| `role` | `"user" \| "assistant" \| "system"` | Required |
| `name?` | `string` |  |
| `time?` | `string` |  |
| `model?` | `string` |  |
| `text?` | `string` |  |
| `streaming?` | `boolean` |  |
| `tint?` | `Tint` |  |
| `sources?` | `Source[]` |  |
| `attachments?` | `{ name: string; size?: string; kind?: string }[]` |  |
| `usage?` | `string` |  |
| `onRetry?` | `() => void` |  |
| `onFeedback?` | `(v: "up" \| "down") => void` |  |
| `actions?` | `boolean` |  |
| `before?` | `ReactNode` |  |
| `after?` | `ReactNode` |  |
| `children?` | `ReactNode` |  |
| `feedback?` | `"up" \| "down" \| null` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
