> ## 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.

# Chat Layout - Web

> Chat layouts structure the primary conversation experience.

> [!NOTE]
>
> For development, import `ChatLayout` 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-chat-layout

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

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

function App() {
  const [value, setValue] = useState("");

  return (
    <div
      style={{
        height: 420,
        border: "1px solid #e0e0e0",
        borderRadius: 8,
        overflow: "hidden",
      }}
    >
      <ChatLayout
        conversation={
          <UserMessage
            content="Hey — can you help with this invoice?"
            timestamp={new Date(2026, 3, 1, 16, 40)}
          />
        }
        prompt={
          <PromptBar
            value={value}
            onChange={setValue}
            placeholder="Ask anything…"
          />
        }
      />
    </div>
  );
}

export default App;
```

## Options

ChatLayout is configuration-first: `conversation` and `prompt` are typed props, not composed children, and both are optional.

### Conversation slot

ai-kit-chat-layout-slots

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

```tsx lines theme={null}
import { type ReactNode, useState } from "react";
import {
  CartoTheme,
  ChatLayout,
  PromptBar,
  UserMessage,
} from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

function Frame({ children }: { children: ReactNode }) {
  return (
    <div
      style={{
        width: 320,
        height: 300,
        border: "1px solid #e0e0e0",
        borderRadius: 8,
        overflow: "hidden",
      }}
    >
      {children}
    </div>
  );
}

function App() {
  const [value, setValue] = useState("");
  const prompt = (
    <PromptBar value={value} onChange={setValue} placeholder="Ask anything…" />
  );
  const conversation = (
    <UserMessage
      content="Hey — can you help with this invoice?"
      timestamp={new Date(2026, 3, 1, 16, 40)}
    />
  );

  return (
    <CartoTheme>
      <div style={{ display: "flex", gap: 16 }}>
        <Frame>
          <ChatLayout conversation={conversation} prompt={prompt} />
        </Frame>
        <Frame>
          <ChatLayout conversation={conversation} />
        </Frame>
        <Frame>
          <ChatLayout prompt={prompt} />
        </Frame>
      </div>
    </CartoTheme>
  );
}

export default App;
```

The three slot configurations side by side: the full column, `conversation` alone (a read-only or archived view, with no composer), and `prompt` alone (an empty landing state, ready for the first message). Either slot can be omitted independently — the other still renders correctly on its own.

### Prompt slot

The `prompt` composer is typically a [PromptBar](https://anvil.servicetitan.com/docs/components/prompt-bar/design), rendered below `conversation` and pinned to the bottom of the region regardless of how much transcript precedes it.

## Behavior

The transcript manages its own scroll position so a growing conversation never fights the reader.

### Stick to Bottom

ai-kit-chat-layout-stick-to-bottom

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

```tsx lines theme={null}
import { useState } from "react";
import {
  CartoTheme,
  ChatLayout,
  PromptBar,
  UserMessage,
} from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

function App() {
  const [value, setValue] = useState("");

  return (
    <CartoTheme>
      <div
        style={{
          height: 420,
          border: "1px solid #e0e0e0",
          borderRadius: 8,
          overflow: "hidden",
        }}
      >
        <ChatLayout
          conversation={
            <>
              {Array.from({ length: 8 }, (_, i) => (
                <UserMessage
                  key={i}
                  content={`Message ${i + 1} in a transcript long enough to overflow the frame.`}
                  timestamp={new Date(2026, 3, 1, 16, 40 + i)}
                />
              ))}
            </>
          }
          prompt={
            <PromptBar
              value={value}
              onChange={setValue}
              placeholder="Ask anything…"
            />
          }
        />
      </div>
    </CartoTheme>
  );
}

export default App;
```

The transcript sticks to the bottom on its own as messages arrive. The moment the user scrolls up to read history, it stops auto-scrolling — so incoming content never yanks them back down — and re-engages the lock once they scroll back to the bottom.

### Scroll to Latest

ai-kit-chat-layout-scroll-to-latest

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

```tsx lines theme={null}
import { useState } from "react";
import {
  CartoTheme,
  ChatLayout,
  PromptBar,
  UserMessage,
} from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

function App() {
  const [value, setValue] = useState("");

  return (
    <CartoTheme>
      <div
        style={{
          height: 420,
          border: "1px solid #e0e0e0",
          borderRadius: 8,
          overflow: "hidden",
        }}
      >
        <ChatLayout
          conversation={
            <>
              {Array.from({ length: 8 }, (_, i) => (
                <UserMessage
                  key={i}
                  content={`Message ${i + 1} in a transcript long enough to overflow the frame.`}
                  timestamp={new Date(2026, 3, 1, 16, 40 + i)}
                />
              ))}
            </>
          }
          prompt={
            <PromptBar
              value={value}
              onChange={setValue}
              placeholder="Ask anything…"
            />
          }
        />
      </div>
    </CartoTheme>
  );
}

export default App;
```

A floating "scroll to latest" button appears whenever the user has scrolled away from the bottom. Pressing it scrolls smoothly back to the latest content and re-engages the stick-to-bottom lock. Force the same scroll programmatically — e.g. right after the user sends a message — via `controlRef`'s `scrollToBottom()`.

## Usage Guidelines

### When to Use

Use the Chat Layout when:

* **Structuring a conversation** – Messages need a scrolling transcript with a composer, such as a [Prompt Bar](https://anvil.servicetitan.com/docs/components/prompt-bar/web.md), pinned below it.
* **Showing a read-only or archived conversation** – Provide only the conversation and omit the composer.
* **Starting an empty conversation** – Provide only the prompt for a landing state ready for the first message.
* **Streaming responses** – Incoming content needs to stay on the latest message without pulling back a user who scrolled up to read history.

### When not to use

Avoid using the Chat Layout for:

* **Arranging the header, history rail, or artifact panel** – Use the [App Shell](https://anvil.servicetitan.com/docs/components/app-shell/web.md) instead, and place the Chat Layout in its chat region.
* **Presenting the experience as a floating or fullscreen panel** – Use the [Window](https://anvil.servicetitan.com/docs/components/window/web.md) instead.

## Keyboard Interaction

Users can scroll the transcript using standard keyboard controls.

| Key | Description |
| - | - |
| Tab | Moves focus into the transcript region |
| Arrow Up / Arrow Down | Scrolls the focused transcript |
| Page Up / Page Down | Scrolls the transcript by a page |

## Patterns that use this component

* [Agentic Experience Shell](https://anvil.servicetitan.com/docs/patterns/agentic-experience-shell/web.md) — Defines the conversation region

## Code

**Implementation**

ai-kit-chat-layout

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

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

function App() {
  const [value, setValue] = useState("");

  return (
    <div
      style={{
        height: 420,
        border: "1px solid #e0e0e0",
        borderRadius: 8,
        overflow: "hidden",
      }}
    >
      <ChatLayout
        conversation={
          <UserMessage
            content="Hey — can you help with this invoice?"
            timestamp={new Date(2026, 3, 1, 16, 40)}
          />
        }
        prompt={
          <PromptBar
            value={value}
            onChange={setValue}
            placeholder="Ask anything…"
          />
        }
      />
    </div>
  );
}

export default App;
```

## Common Examples

To use the `ChatLayout`, give it a parent with a real height and pass `conversation` and `prompt` as configured slots. The kit forbids `children`. The column fills a narrow region and caps at 768px when wider.

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

function ExampleComponent() {
  return (
    <ChatLayout
      conversation={<UserMessage content="Hello" timestamp={sentAt} />}
      prompt={<PromptBar value={value} onChange={setValue} />}
    />
  );
}
```

### Slots

Both slots are optional. Omit `conversation` for a prompt-only landing. Omit `prompt` for a transcript with no composer. The prompt stays bottom-pinned in both cases.

ai-kit-chat-layout-slots

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

```tsx lines expandable theme={null}
import { type ReactNode, useState } from "react";
import {
  CartoTheme,
  ChatLayout,
  PromptBar,
  UserMessage,
} from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

function Frame({ children }: { children: ReactNode }) {
  return (
    <div
      style={{
        width: 320,
        height: 300,
        border: "1px solid #e0e0e0",
        borderRadius: 8,
        overflow: "hidden",
      }}
    >
      {children}
    </div>
  );
}

function App() {
  const [value, setValue] = useState("");
  const prompt = (
    <PromptBar value={value} onChange={setValue} placeholder="Ask anything…" />
  );
  const conversation = (
    <UserMessage
      content="Hey — can you help with this invoice?"
      timestamp={new Date(2026, 3, 1, 16, 40)}
    />
  );

  return (
    <CartoTheme>
      <div style={{ display: "flex", gap: 16 }}>
        <Frame>
          <ChatLayout conversation={conversation} prompt={prompt} />
        </Frame>
        <Frame>
          <ChatLayout conversation={conversation} />
        </Frame>
        <Frame>
          <ChatLayout prompt={prompt} />
        </Frame>
      </div>
    </CartoTheme>
  );
}

export default App;
```

### Stick to bottom

New messages stick the transcript to the bottom. Scrolling up to read history stops auto-scroll. Scrolling back to the bottom re-engages the lock. A floating **scroll to latest** button appears while the user is not at the bottom.

ai-kit-chat-layout-stick-to-bottom

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

```tsx lines expandable theme={null}
import { useState } from "react";
import {
  CartoTheme,
  ChatLayout,
  PromptBar,
  UserMessage,
} from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

function App() {
  const [value, setValue] = useState("");

  return (
    <CartoTheme>
      <div
        style={{
          height: 420,
          border: "1px solid #e0e0e0",
          borderRadius: 8,
          overflow: "hidden",
        }}
      >
        <ChatLayout
          conversation={
            <>
              {Array.from({ length: 8 }, (_, i) => (
                <UserMessage
                  key={i}
                  content={`Message ${i + 1} in a transcript long enough to overflow the frame.`}
                  timestamp={new Date(2026, 3, 1, 16, 40 + i)}
                />
              ))}
            </>
          }
          prompt={
            <PromptBar
              value={value}
              onChange={setValue}
              placeholder="Ask anything…"
            />
          }
        />
      </div>
    </CartoTheme>
  );
}

export default App;
```

### Scroll after send

The layout cannot infer when the user sends. Pass `controlRef` and call `scrollToBottom()` after send so the transcript snaps to the latest regardless of scroll position.

ai-kit-chat-layout-scroll-to-latest

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

```tsx lines expandable theme={null}
import { useState } from "react";
import {
  CartoTheme,
  ChatLayout,
  PromptBar,
  UserMessage,
} from "@servicetitan/anvil2-ai-kit";
import "@servicetitan/anvil2-ai-kit/styles.css";

function App() {
  const [value, setValue] = useState("");

  return (
    <CartoTheme>
      <div
        style={{
          height: 420,
          border: "1px solid #e0e0e0",
          borderRadius: 8,
          overflow: "hidden",
        }}
      >
        <ChatLayout
          conversation={
            <>
              {Array.from({ length: 8 }, (_, i) => (
                <UserMessage
                  key={i}
                  content={`Message ${i + 1} in a transcript long enough to overflow the frame.`}
                  timestamp={new Date(2026, 3, 1, 16, 40 + i)}
                />
              ))}
            </>
          }
          prompt={
            <PromptBar
              value={value}
              onChange={setValue}
              placeholder="Ask anything…"
            />
          }
        />
      </div>
    </CartoTheme>
  );
}

export default App;
```

```tsx theme={null}
const controlRef = useRef<ChatLayoutHandle>(null);

function send() {
  appendUserMessage(value);
  controlRef.current?.scrollToBottom();
}

return <ChatLayout controlRef={controlRef} conversation={…} prompt={…} />;
```

## Internationalization

`ChatLayout` owns these localized strings:

* `ai-kit.chatLayout.conversation`
* `ai-kit.chatLayout.scrollToBottom`

Mount `AiKitIntlProvider` inside `CartoTheme` to switch them.

## React Accessibility

* The transcript is a `role="log"` live region. Appended messages are announced.
* Render streaming token nodes as non-live and let the final message land in the log.
* `controlRef.current.isAtBottom` mirrors whether the scroll-to-latest button is shown.

**ChatLayout Props**

```tsx theme={null}
<ChatLayout
  conversation={messages}
  prompt={<PromptBar value={value} onChange={setValue} />}
  controlRef={controlRef}
/>
```

## `ChatLayout` Props

The `ChatLayout` accepts the following props and forwards remaining `<div>` attributes except `children` and `style`.

**className** `string`

Class merged onto the column, not replaced.

**controlRef** `Ref\<ChatLayoutHandle\>`

Imperative handle for `scrollToBottom()` and `isAtBottom`.

**conversation** `ReactNode`

Transcript rendered above the prompt in a scrolling log. Omit for a
prompt-only landing.

**prompt** `ReactNode`

Composer pinned to the bottom. Typically a `PromptBar`. Omit to render
the transcript alone.

**ChatLayoutHandle**

```tsx theme={null}
controlRef.current?.scrollToBottom();
```

## `ChatLayoutHandle`

**isAtBottom** `boolean` *required*

Whether the transcript is locked to the bottom. Mirrors the scroll-to-latest
button's visibility.

**scrollToBottom** `() =\> void` *required*

Smoothly scrolls to the latest content and re-engages the stick-to-bottom
lock. Call after the user sends.


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