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

# Window - Web

> Windows present contained, moveable or framed content regions.

> [!NOTE]
>
> For development, import `Window` 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-window

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

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

function App() {
  return (
    <div style={{ position: "fixed", inset: 0, background: "#e9ecf1" }}>
      <Window
        chat={
          <div
            style={{
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              height: "100%",
            }}
          >
            <Text text="Chat content" />
          </div>
        }
      />
    </div>
  );
}

export default App;
```

## Options

Window owns the same slots as [AppShell](https://anvil.servicetitan.com/docs/components/app-shell/design) — `header` and `chat` (shown in both states), plus `rail` and `artifact` (fullscreen only) — and presents them in one of two sizes.

### Window States

ai-kit-window-window-states

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

```tsx lines theme={null}
import {
  AppHeader,
  CartoTheme,
  Text,
  Window,
  useAppSurface,
} from "@servicetitan/anvil2-ai-kit";
import { IconMinimize2, IconX } from "@servicetitan/anvil2-icons";
import "@servicetitan/anvil2-ai-kit/styles.css";

function WindowHeader() {
  const { toggleSize, close } = useAppSurface();
  return (
    <AppHeader
      title="Title of conversation"
      trailingActions={[
        {
          id: "size",
          icon: <IconMinimize2 />,
          label: "Restore",
          onPress: toggleSize,
        },
        { id: "close", icon: <IconX />, label: "Close", onPress: close },
      ]}
    />
  );
}

function App() {
  return (
    <CartoTheme>
      <div style={{ position: "fixed", inset: 0, background: "#e9ecf1" }}>
        <Window
          defaultState="fullscreen"
          header={<WindowHeader />}
          chat={
            <div
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                height: "100%",
              }}
            >
              <Text text="Chat content" />
            </div>
          }
        />
      </div>
    </CartoTheme>
  );
}

export default App;
```

**Docked** (shown above) is a fixed-size floating panel — header + chat, anchored bottom-right by default — that sits non-modally over the rest of the page. **Fullscreen** (shown here) is a modal takeover revealing the full `AppShell`: header, rail, chat, and artifact, over a scrim, with focus trapped inside. The `header` and `chat` nodes are the same React instance in both states — same DOM, scroll position, and in-flight state — so expanding or restoring never resets the conversation.

### Open Control

ai-kit-window-open-control

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

```tsx lines theme={null}
import { useRef } from "react";
import {
  AppHeader,
  Button,
  CartoTheme,
  Text,
  Window,
  useAppSurface,
} from "@servicetitan/anvil2-ai-kit";
import type { AppSurfaceHandle } from "@servicetitan/anvil2-ai-kit";
import { IconMaximize2, IconX } from "@servicetitan/anvil2-icons";
import "@servicetitan/anvil2-ai-kit/styles.css";

function WindowHeader() {
  const { toggleSize, close } = useAppSurface();
  return (
    <AppHeader
      title="Title of conversation"
      trailingActions={[
        {
          id: "size",
          icon: <IconMaximize2 />,
          label: "Maximize",
          onPress: toggleSize,
        },
        { id: "close", icon: <IconX />, label: "Close", onPress: close },
      ]}
    />
  );
}

function App() {
  const ctrl = useRef<AppSurfaceHandle>(null);

  return (
    <CartoTheme>
      <div style={{ position: "fixed", inset: 0, background: "#e9ecf1" }}>
        <div style={{ position: "fixed", top: 24, left: 24 }}>
          <Button label="Open chat" onPress={() => ctrl.current?.open()} />
        </div>
        <Window
          controlRef={ctrl}
          header={<WindowHeader />}
          chat={
            <div
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                height: "100%",
              }}
            >
              <Text text="Chat content" />
            </div>
          }
        />
      </div>
    </CartoTheme>
  );
}

export default App;
```

Open/close and docked/fullscreen work uncontrolled by default (seeded by `defaultOpen` / `defaultState`), or controlled by passing `open` / `state` alongside `onOpenChange` / `onStateChange` so an external store can be the source of truth. Either way, drive the same transitions imperatively via the `AppSurfaceHandle` — from a parent through `controlRef` (as shown here, with an external launcher button), or from inside the window (a header button, or a card in the chat) via `useAppSurface()`.

## Behavior

Both states share one surface, so the panel animates between them, and the docked panel can be repositioned.

### Maximize and Restore

ai-kit-window-maximize-and-restore

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

```tsx lines theme={null}
import {
  AppHeader,
  CartoTheme,
  Text,
  Window,
  useAppSurface,
} from "@servicetitan/anvil2-ai-kit";
import { IconMaximize2, IconX } from "@servicetitan/anvil2-icons";
import "@servicetitan/anvil2-ai-kit/styles.css";

function WindowHeader() {
  const { toggleSize, close } = useAppSurface();
  return (
    <AppHeader
      title="Title of conversation"
      trailingActions={[
        {
          id: "size",
          icon: <IconMaximize2 />,
          label: "Maximize",
          onPress: toggleSize,
        },
        { id: "close", icon: <IconX />, label: "Close", onPress: close },
      ]}
    />
  );
}

function App() {
  return (
    <CartoTheme>
      <div style={{ position: "fixed", inset: 0, background: "#e9ecf1" }}>
        <Window
          header={<WindowHeader />}
          chat={
            <div
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                height: "100%",
              }}
            >
              <Text text="Chat content" />
            </div>
          }
        />
      </div>
    </CartoTheme>
  );
}

export default App;
```

The header's maximize control expands the docked panel into the fullscreen modal shell; restore (or Esc) shrinks it back. Docked and fullscreen share one surface, so the panel visibly grows and shrinks between the two rects rather than swapping instantly.

### Drag

ai-kit-window-drag

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

```tsx lines theme={null}
import {
  AppHeader,
  CartoTheme,
  Text,
  Window,
  useAppSurface,
} from "@servicetitan/anvil2-ai-kit";
import { IconGrip, IconMaximize2, IconX } from "@servicetitan/anvil2-icons";
import "@servicetitan/anvil2-ai-kit/styles.css";

// A marker at the panel's default anchor (bottom-right, 24px inset — matching
// the docked panel's own resting corner) so a screenshot of the dragged panel
// still shows where it started, proving it moved.
function DefaultCornerMarker() {
  return (
    <div
      style={{
        position: "fixed",
        insetBlockEnd: 24,
        insetInlineEnd: 24,
        width: 120,
        height: 48,
        border: "2px dashed #99a3ad",
        borderRadius: 8,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <Text size="fine" text="Default corner" />
    </div>
  );
}

function WindowHeader() {
  const { toggleSize, close, dragHandleProps } = useAppSurface();
  return (
    <AppHeader
      title="Title of conversation"
      leadingActions={[
        {
          id: "drag",
          icon: <IconGrip />,
          label: "Move window",
          dragHandleProps,
        },
      ]}
      trailingActions={[
        {
          id: "size",
          icon: <IconMaximize2 />,
          label: "Maximize",
          onPress: toggleSize,
        },
        { id: "close", icon: <IconX />, label: "Close", onPress: close },
      ]}
    />
  );
}

function App() {
  return (
    <CartoTheme>
      <div style={{ position: "fixed", inset: 0, background: "#e9ecf1" }}>
        <DefaultCornerMarker />
        <Window
          header={<WindowHeader />}
          chat={
            <div
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                height: "100%",
              }}
            >
              <Text text="Chat content" />
            </div>
          }
        />
      </div>
    </CartoTheme>
  );
}

export default App;
```

Docked, the panel is draggable: spread the surface's `dragHandleProps` (from `controlRef` / `useAppSurface()`) onto a leading header action — here an `IconGrip` — to turn it into a grip that moves the panel anywhere in the viewport. Arrow keys nudge it too, for keyboard users. The dragged position survives a maximize→restore round-trip (restore returns to it) and resets to the default corner only on close.

### Scrim Dismiss

Fullscreen is modal: a scrim sits behind the shell and dismisses back to docked on click, the same as pressing Esc. Because the shell fills the viewport, the scrim itself has no visible edge — the affordance is the dismissal, not a visible backdrop.

## Usage Guidelines

### When to Use

Use the Window when:

* **Chat over a product page** – The agent experience opens over another page, and context from that page remains important. Default to the docked state.
* **Switching between docked and fullscreen** – Users move between a floating panel and a fullscreen shell without losing the conversation, scroll position, or in-flight state.
* **Focused workflows** – A workflow requires dedicated focus or an artifact. Default to the fullscreen state.

### When not to use

Avoid using the Window for:

* **An artifact beside a docked chat** – The artifact region renders only in the fullscreen state. Open the Window fullscreen when the artifact must be visible.
* **Confirmations or acknowledgements** – The content is a decision or message the user confirms or dismisses, not a conversation. Use the [Dialog](https://anvil.servicetitan.com/docs/components/dialog/ai-kit.md) instead.

## Keyboard Interaction

Users can maximize, restore, move, and dismiss a window using standard keyboard controls.

| Key | Description |
| - | - |
| Tab | Moves focus among header actions and chat content |
| Enter / Space | Activates the focused action |
| Arrow keys | Moves the docked panel when the drag handle is focused |
| Escape | Restores a fullscreen window to docked |

## Patterns that use this component

* [Agentic Experience Shell](https://anvil.servicetitan.com/docs/patterns/agentic-experience-shell/web.md) — Presents the shell as a floating or fullscreen experience
* [Atlas Entry Points](https://anvil.servicetitan.com/docs/patterns/atlas-entry-points/web.md) — Where a user begins or resumes an Atlas conversation

## Code

**Implementation**

ai-kit-window

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

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

function App() {
  return (
    <div style={{ position: "fixed", inset: 0, background: "#e9ecf1" }}>
      <Window
        chat={
          <div
            style={{
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              height: "100%",
            }}
          >
            <Text text="Chat content" />
          </div>
        }
      />
    </div>
  );
}

export default App;
```

## Common Examples

To use the `Window`, pass the required `chat` slot. Add `header`, `rail`, and `artifact` as needed. Docked, the window shows `header` and `chat`. Fullscreen, it reveals the full shell. `header` and `chat` keep the same React instance across the transition.

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

function ExampleComponent() {
  return (
    <Window
      header={<AppHeader title="Atlas" />}
      chat={<ChatLayout conversation={messages} prompt={prompt} />}
      rail={<ChatHistory items={chats} />}
      artifact={<TabbedLayout tabs={tabs} />}
    />
  );
}
```

### Open control

Uncontrolled, the window owns open state, seeded by `defaultOpen` (`true`). Pass `open` and `onOpenChange` to control it from a store.

ai-kit-window-open-control

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

```tsx lines expandable theme={null}
import { useRef } from "react";
import {
  AppHeader,
  Button,
  CartoTheme,
  Text,
  Window,
  useAppSurface,
} from "@servicetitan/anvil2-ai-kit";
import type { AppSurfaceHandle } from "@servicetitan/anvil2-ai-kit";
import { IconMaximize2, IconX } from "@servicetitan/anvil2-icons";
import "@servicetitan/anvil2-ai-kit/styles.css";

function WindowHeader() {
  const { toggleSize, close } = useAppSurface();
  return (
    <AppHeader
      title="Title of conversation"
      trailingActions={[
        {
          id: "size",
          icon: <IconMaximize2 />,
          label: "Maximize",
          onPress: toggleSize,
        },
        { id: "close", icon: <IconX />, label: "Close", onPress: close },
      ]}
    />
  );
}

function App() {
  const ctrl = useRef<AppSurfaceHandle>(null);

  return (
    <CartoTheme>
      <div style={{ position: "fixed", inset: 0, background: "#e9ecf1" }}>
        <div style={{ position: "fixed", top: 24, left: 24 }}>
          <Button label="Open chat" onPress={() => ctrl.current?.open()} />
        </div>
        <Window
          controlRef={ctrl}
          header={<WindowHeader />}
          chat={
            <div
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                height: "100%",
              }}
            >
              <Text text="Chat content" />
            </div>
          }
        />
      </div>
    </CartoTheme>
  );
}

export default App;
```

### Maximize and restore

Uncontrolled size is seeded by `defaultState` (`"docked"`). Pass `state` and `onStateChange` to own it. `useAppSurface().maximize()` and `restore()` drive the same transitions.

ai-kit-window-maximize-and-restore

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

```tsx lines expandable theme={null}
import {
  AppHeader,
  CartoTheme,
  Text,
  Window,
  useAppSurface,
} from "@servicetitan/anvil2-ai-kit";
import { IconMaximize2, IconX } from "@servicetitan/anvil2-icons";
import "@servicetitan/anvil2-ai-kit/styles.css";

function WindowHeader() {
  const { toggleSize, close } = useAppSurface();
  return (
    <AppHeader
      title="Title of conversation"
      trailingActions={[
        {
          id: "size",
          icon: <IconMaximize2 />,
          label: "Maximize",
          onPress: toggleSize,
        },
        { id: "close", icon: <IconX />, label: "Close", onPress: close },
      ]}
    />
  );
}

function App() {
  return (
    <CartoTheme>
      <div style={{ position: "fixed", inset: 0, background: "#e9ecf1" }}>
        <Window
          header={<WindowHeader />}
          chat={
            <div
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                height: "100%",
              }}
            >
              <Text text="Chat content" />
            </div>
          }
        />
      </div>
    </CartoTheme>
  );
}

export default App;
```

### Window states

Docked is a non-modal floating panel. Fullscreen is a modal takeover over a scrim. Esc or scrim click returns to docked.

ai-kit-window-window-states

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

```tsx lines expandable theme={null}
import {
  AppHeader,
  CartoTheme,
  Text,
  Window,
  useAppSurface,
} from "@servicetitan/anvil2-ai-kit";
import { IconMinimize2, IconX } from "@servicetitan/anvil2-icons";
import "@servicetitan/anvil2-ai-kit/styles.css";

function WindowHeader() {
  const { toggleSize, close } = useAppSurface();
  return (
    <AppHeader
      title="Title of conversation"
      trailingActions={[
        {
          id: "size",
          icon: <IconMinimize2 />,
          label: "Restore",
          onPress: toggleSize,
        },
        { id: "close", icon: <IconX />, label: "Close", onPress: close },
      ]}
    />
  );
}

function App() {
  return (
    <CartoTheme>
      <div style={{ position: "fixed", inset: 0, background: "#e9ecf1" }}>
        <Window
          defaultState="fullscreen"
          header={<WindowHeader />}
          chat={
            <div
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                height: "100%",
              }}
            >
              <Text text="Chat content" />
            </div>
          }
        />
      </div>
    </CartoTheme>
  );
}

export default App;
```

### Drag

Spread `dragHandleProps` from `useAppSurface()` onto a leading header action. The dragged position survives maximize and restore, and resets to the default corner only on close. The props are inert when fullscreen.

ai-kit-window-drag

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

```tsx lines expandable theme={null}
import {
  AppHeader,
  CartoTheme,
  Text,
  Window,
  useAppSurface,
} from "@servicetitan/anvil2-ai-kit";
import { IconGrip, IconMaximize2, IconX } from "@servicetitan/anvil2-icons";
import "@servicetitan/anvil2-ai-kit/styles.css";

// A marker at the panel's default anchor (bottom-right, 24px inset — matching
// the docked panel's own resting corner) so a screenshot of the dragged panel
// still shows where it started, proving it moved.
function DefaultCornerMarker() {
  return (
    <div
      style={{
        position: "fixed",
        insetBlockEnd: 24,
        insetInlineEnd: 24,
        width: 120,
        height: 48,
        border: "2px dashed #99a3ad",
        borderRadius: 8,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <Text size="fine" text="Default corner" />
    </div>
  );
}

function WindowHeader() {
  const { toggleSize, close, dragHandleProps } = useAppSurface();
  return (
    <AppHeader
      title="Title of conversation"
      leadingActions={[
        {
          id: "drag",
          icon: <IconGrip />,
          label: "Move window",
          dragHandleProps,
        },
      ]}
      trailingActions={[
        {
          id: "size",
          icon: <IconMaximize2 />,
          label: "Maximize",
          onPress: toggleSize,
        },
        { id: "close", icon: <IconX />, label: "Close", onPress: close },
      ]}
    />
  );
}

function App() {
  return (
    <CartoTheme>
      <div style={{ position: "fixed", inset: 0, background: "#e9ecf1" }}>
        <DefaultCornerMarker />
        <Window
          header={<WindowHeader />}
          chat={
            <div
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                height: "100%",
              }}
            >
              <Text text="Chat content" />
            </div>
          }
        />
      </div>
    </CartoTheme>
  );
}

export default App;
```

### Region intents

`showArtifact()` and `showRail()` from a docked window maximize first, then open the region, in one call. The same `AppSurfaceHandle` is documented on `AppShell`.

## Internationalization

`Window` owns `ai-kit.window.label` as the accessible name of the fullscreen dialog. Override it with `aria-label`. Region chrome uses the `AppShell` message IDs.

## React Accessibility

* Fullscreen traps focus over a scrim. Esc restores to docked.
* Docked drag also supports arrow-key nudging through `dragHandleProps`.
* The fullscreen dialog's accessible name defaults to the localized window label.

**Window Props**

```tsx theme={null}
<Window
  header={header}
  chat={chat}
  rail={rail}
  artifact={artifact}
  open={open}
  onOpenChange={setOpen}
  state={state}
  onStateChange={setState}
/>
```

## `Window` Props

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

**chat** `ReactNode` *required*

Center conversation. Preserved across docked and fullscreen.

**artifact** `ReactNode`

Fullscreen-only right region. Omit to render no artifact.

**className** `string`

Class merged onto the root. Override docked corner or size here.

**controlRef** `Ref\<AppSurfaceHandle\>`

Imperative surface handle for a parent. Prefer `useAppSurface` inside the
tree.

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

Starts the fullscreen artifact collapsed on first paint.

**defaultOpen** `boolean` (default: true)

Initial open state when uncontrolled.

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

Starts the fullscreen rail collapsed on first paint.

**defaultState** `"docked" | "fullscreen"` (default: docked)

Initial size state when uncontrolled.

**header** `ReactNode`

Header shown in both states and preserved across the transition.

**onOpenChange** `(open: boolean) =\> void`

Fired whenever open state changes. Required to drive a controlled window.

**onStateChange** `(state: "docked" | "fullscreen") =\> void`

Fired whenever size state changes. Required to drive a controlled window.

**open** `boolean`

Controlled open state.

**rail** `ReactNode`

Fullscreen-only left region. Omit to render no rail.

**state** `"docked" | "fullscreen"`

Controlled size state.


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