> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# User Message - Web

> User messages display content sent by the user in a conversation.

> [!NOTE]
>
> For development, import `UserMessage` from
> `@servicetitan/anvil2-ai-kit`. For design, find it in the
> [AI Kit Web Component Library file](https://www.figma.com/design/QEIsArLktLeJAaFvcpw3PO/AI-Kit-Web-Component-Library).

ai-kit-user-message

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-user-message?file=src/App.tsx)

```tsx lines theme={null}
import { UserMessage } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <UserMessage
      content="Hey — can you take a look at this?"
      timestamp={new Date(2026, 3, 1, 16, 40)}
    />
  );
}

export default App;
```

## Options

UserMessage supports arbitrary content and an optional error line with a retry action.

### Message Content

ai-kit-user-message-content-length

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-user-message-content-length?file=src/App.tsx)

```tsx lines theme={null}
import { UserMessage } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: 16,
        maxWidth: "48rem",
      }}
    >
      {/* Short */}
      <UserMessage
        content="Hey — can you take a look at this?"
        timestamp={new Date(2026, 3, 1, 16, 40)}
      />

      {/* Long — wraps at the 640px cap */}
      <UserMessage
        content="Here's a longer message that exceeds the comfortable single-line width of the bubble, so it wraps onto multiple lines and never exceeds the 640px cap."
        timestamp={new Date(2026, 3, 1, 16, 41)}
      />
    </div>
  );
}

export default App;
```

The bubble caps at 640px and wraps long content; short messages stay compact. `content` accepts any `ReactNode` — a plain string, inline composition (Carto `Link`, `strong`, etc.), or a rendered Markdown tree.

### Error

ai-kit-user-message-error

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-user-message-error?file=src/App.tsx)

```tsx lines theme={null}
import { UserMessage } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <UserMessage
      content="Can you look at this?"
      error="Message not delivered."
      onRetry={() => {}}
      timestamp={new Date(2026, 3, 1, 16, 40)}
    />
  );
}

export default App;
```

`error` renders a message below the bubble. Pair it with `onRetry` to add a retry action to the toolbar — the button appears only when both are set.

## Behavior

The toolbar's actions and visibility both respond to the message's content and its position in the conversation.

### Content Length

The bubble never truncates or clips its content — it only wraps. There's no maximum height or line count; a long message simply grows taller.

### Toolbar Actions

ai-kit-user-message-toolbar-actions

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-user-message-toolbar-actions?file=src/App.tsx)

```tsx lines theme={null}
import { UserMessage } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: 16,
        maxWidth: "48rem",
      }}
    >
      {/* Copy only */}
      <UserMessage
        content="Hey — can you take a look at this?"
        timestamp={new Date(2026, 3, 1, 16, 40)}
      />

      {/* Copy + retry */}
      <UserMessage
        content="Can you look at this?"
        error="Message not delivered."
        onRetry={() => {}}
        timestamp={new Date(2026, 3, 1, 16, 41)}
      />
    </div>
  );
}

export default App;
```

The toolbar always shows the timestamp, plus a copy action whenever there's text to copy; retry joins them only when `error` is also set. Copy is text-only — pass `copyText` for rich content (Markdown, fragments, images) to keep the message copyable; a message with no text shows no copy button.

### Reveal Toolbar on Hover

ai-kit-user-message-reveal-toolbar-on-hover

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-user-message-reveal-toolbar-on-hover?file=src/App.tsx)

```tsx lines theme={null}
import { UserMessage } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: 12,
        maxWidth: "48rem",
      }}
    >
      {/* Earlier message — toolbar hidden until hover or focus (pinned to its hover state here) */}
      <UserMessage
        content="First message — hover to reveal the toolbar."
        timestamp={new Date(2026, 3, 1, 16, 35)}
        revealToolbarOnHover
      />

      {/* Latest message — toolbar always visible */}
      <UserMessage
        content="Latest message — toolbar always visible."
        timestamp={new Date(2026, 3, 1, 16, 40)}
      />
    </div>
  );
}

export default App;
```

Set `revealToolbarOnHover` on every message except the most recent of its type, so only the latest keeps its toolbar always visible — earlier messages reveal theirs on hover or keyboard focus. This distinction doesn't apply on touch (coarse-pointer) devices, which always show the toolbar regardless of `revealToolbarOnHover`.

## Usage Guidelines

### When to Use

Use the User Message when:

* **User-sent content** – A conversation shows a message the user sent, in a right-aligned bubble.
* **Rich user content** – The message includes inline links, formatting, or a rendered Markdown tree. Pass copy text so the copy action stays available.
* **Failed sends** – A message did not deliver and the user needs an error line with a retry action.

### When not to use

Avoid using the User Message for:

* **Agent responses** – The agent authored the message. Use the [Agent Message](https://anvil.servicetitan.com/docs/components/agent-message/web.md) instead.
* **Media attachments** – The user sent a file, image, video, or audio attachment that needs a preview or playback. Use the [User Media Message](https://anvil.servicetitan.com/docs/components/user-media-message/web.md) instead.

## Keyboard Interaction

Users can navigate and activate toolbar actions using standard keyboard controls; focus also reveals a hidden toolbar.

| Key | Description |
| - | - |
| Tab | Moves focus into the toolbar, revealing it if hidden |
| Arrow keys | Moves focus between toolbar actions |
| Enter / Space | Activates the focused action |

## Code

**Implementation**

ai-kit-user-message

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-user-message?file=src/App.tsx)

```tsx lines expandable theme={null}
import { UserMessage } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <UserMessage
      content="Hey — can you take a look at this?"
      timestamp={new Date(2026, 3, 1, 16, 40)}
    />
  );
}

export default App;
```

## Common Examples

To use the `UserMessage`, pass `content` and a required `timestamp`. The bubble right-aligns with a 24px left gutter. The kit forbids `children`.

```tsx theme={null}
import { UserMessage } from "@servicetitan/anvil2-ai-kit";

function ExampleComponent() {
  return (
    <UserMessage
      content="Can you look at this invoice?"
      timestamp={sentAt}
    />
  );
}
```

`content` accepts a string, inline markup, or a Markdown tree. Copy is text-only: pass `copyText` when `content` is rich so the copy action stays available. When neither yields text, the copy button is omitted.

### Content length

Long messages wrap inside the bubble.

ai-kit-user-message-content-length

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-user-message-content-length?file=src/App.tsx)

```tsx lines expandable theme={null}
import { UserMessage } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: 16,
        maxWidth: "48rem",
      }}
    >
      {/* Short */}
      <UserMessage
        content="Hey — can you take a look at this?"
        timestamp={new Date(2026, 3, 1, 16, 40)}
      />

      {/* Long — wraps at the 640px cap */}
      <UserMessage
        content="Here's a longer message that exceeds the comfortable single-line width of the bubble, so it wraps onto multiple lines and never exceeds the 640px cap."
        timestamp={new Date(2026, 3, 1, 16, 41)}
      />
    </div>
  );
}

export default App;
```

### Error and retry

Pass `error` to render a line below the bubble. `onRetry` shows a refresh action only when `error` is also set.

ai-kit-user-message-error

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-user-message-error?file=src/App.tsx)

```tsx lines expandable theme={null}
import { UserMessage } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <UserMessage
      content="Can you look at this?"
      error="Message not delivered."
      onRetry={() => {}}
      timestamp={new Date(2026, 3, 1, 16, 40)}
    />
  );
}

export default App;
```

### Toolbar

The toolbar always includes copy (when copyable) and the formatted timestamp. Set `revealToolbarOnHover` so a chat list can hide the toolbar until hover or focus, typically on every message except the latest. Coarse pointers always show the toolbar. Keyboard focus reveals it.

ai-kit-user-message-reveal-toolbar-on-hover

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-user-message-reveal-toolbar-on-hover?file=src/App.tsx)

```tsx lines expandable theme={null}
import { UserMessage } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: 12,
        maxWidth: "48rem",
      }}
    >
      {/* Earlier message — toolbar hidden until hover or focus (pinned to its hover state here) */}
      <UserMessage
        content="First message — hover to reveal the toolbar."
        timestamp={new Date(2026, 3, 1, 16, 35)}
        revealToolbarOnHover
      />

      {/* Latest message — toolbar always visible */}
      <UserMessage
        content="Latest message — toolbar always visible."
        timestamp={new Date(2026, 3, 1, 16, 40)}
      />
    </div>
  );
}

export default App;
```

ai-kit-user-message-toolbar-actions

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-user-message-toolbar-actions?file=src/App.tsx)

```tsx lines expandable theme={null}
import { UserMessage } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: 16,
        maxWidth: "48rem",
      }}
    >
      {/* Copy only */}
      <UserMessage
        content="Hey — can you take a look at this?"
        timestamp={new Date(2026, 3, 1, 16, 40)}
      />

      {/* Copy + retry */}
      <UserMessage
        content="Can you look at this?"
        error="Message not delivered."
        onRetry={() => {}}
        timestamp={new Date(2026, 3, 1, 16, 41)}
      />
    </div>
  );
}

export default App;
```

## Internationalization

The action toolbar owns these localized strings:

* `ai-kit.messageToolbar.copy`
* `ai-kit.messageToolbar.copied`
* `ai-kit.messageToolbar.retry`
* `ai-kit.messageToolbar.userActions`

Localize `content` and `error` before passing them. Mount `AiKitIntlProvider` inside `CartoTheme` to switch the toolbar strings.

## React Accessibility

* The toolbar groups user message actions under a localized name.
* Timestamp is always present in the toolbar.
* Hover-hidden toolbars remain reachable from the keyboard.

**UserMessage Props**

```tsx theme={null}
<UserMessage
  content="Can you look at this invoice?"
  timestamp={sentAt}
  copyText="Can you look at this invoice?"
/>
```

## `UserMessage` Props

The `UserMessage` accepts the following props and forwards remaining `<div>` attributes except `children`, HTML `content`, and `style`.

**content** `ReactNode` *required*

Message body. String, inline markup, or a Markdown tree.

**timestamp** `Date | string | number` *required*

Send time. Accepts a `Date`, ISO string, or epoch number.

**copyText** `string`

Source text for copy. Takes precedence over a string `content`.

**error** `string`

Error line below the bubble. Omit or pass empty to hide it.

**onRetry** `() =\> void | Promise\<void\>`

Retry a failed send. Surfaces only when `error` is set.

**revealToolbarOnHover** `boolean` (default: false)

Hides the toolbar until hover or focus. Touch always shows it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.