ComponentsAgent patterns

Chat Window

The frame for a quick chat that opens over any page without leaving it. It holds a header, your chat, and a message box. Switch it to a side drawer or a full page, or minimize it back into its launcher. The page behind stays usable the whole time.

Open as window

Operations

Good morning

Three orders ship today. Bud keeps one private chat you can open from any page.

Order 4471 · Northwind Paper
Order 4480 · Harbor Glass
Order 4492 · Fern & Co

Usage

Chat Window usageTSX
import {
  ChatWindow,
  ChatWindowActions,
  ChatWindowBody,
  ChatWindowClose,
  ChatWindowContent,
  ChatWindowFooter,
  ChatWindowHeader,
  ChatWindowLauncher,
  ChatWindowLayoutMenu,
  ChatWindowTitle,
} from "@tulipfarm/sprout/chat-window";

<ChatWindow open={open} onOpenChange={setOpen} layout={layout} onLayoutChange={setLayout}>
  <ChatWindowLauncher>
    <AgentAvatar agentId="bud" name="Bud" size="sm" />
    Bud
  </ChatWindowLauncher>
  <ChatWindowContent>
    <ChatWindowHeader>
      <ChatWindowTitle>Late supplier deliveries</ChatWindowTitle>
      <ChatWindowActions>
        <ChatWindowLayoutMenu />
        <ChatWindowClose />
      </ChatWindowActions>
    </ChatWindowHeader>
    <ChatWindowBody>
      <MessageScrollerProvider>…</MessageScrollerProvider>
    </ChatWindowBody>
    <ChatWindowFooter>
      <ChatInput … />
    </ChatWindowFooter>
  </ChatWindowContent>
</ChatWindow>

Chat Window is only the frame. Put your existing chat in it: a MessageScroller in ChatWindowBody and a ChatInput in ChatWindowFooter. It works controlled with open and layout, or uncontrolled with defaultOpen and defaultLayout.

Layouts

layout is "window" (default), "drawer", or "page". A window floats in its corner at 34rem by 44rem and shrinks to fit. A drawer is 28rem wide and fills the height of the corner's side. A full page covers the viewport and keeps the thread and message box in a 48rem column. Below 640px every layout fills the screen, so the menu offers only Full page and Minimize.

Your chat stays mounted while the window is minimized, so drafts, scroll position, and a running reply survive. Set keepMounted={false} on ChatWindowContent to unmount it instead.

Open the full chat page

If your app has its own chat page, go there when someone picks Full page, and leave the layout as it was. When they come back, the window is where they left it.

Full page opens a routeTSX
<ChatWindow
  open={open}
  onOpenChange={setOpen}
  layout={layout}
  onLayoutChange={next => {
    if (next === "page") navigate(`/chat/${chatId}`);
    else setLayout(next);
  }}
>

To offer fewer layouts, pass them to the menu: <ChatWindowLayoutMenu layouts={["window", "page"]} />.labels renames any item, and showMinimize={false} removes Minimize.

Corners and your own launcher

corner is "bottom-right" (default), "bottom-left", "top-right", or "top-left". The launcher and window sit in that corner, the window grows out of it, and a drawer fills that side. ChatWindowLauncher is an outline pill that hides while the window is open. To open the window from a header button or a draggable launcher of your own, use ChatWindowTrigger with render.

To keep the window inside one region of the page, as this demo does, pass that region as container and give it contain: layout. The window and launcher then sit in the region's corner instead of the viewport's.

API

ChatWindow wraps a non-modal Base UI dialog and takes its props, plus layout, defaultLayout, onLayoutChange, and corner. It always sets modal={false} and ignores clicks outside, so the window only closes from its own controls or Escape.

ChatWindowContent takes keepMounted (default true), container, and every Base UI popup prop. It sets data-slot="chat-window-content", data-layout, and data-corner. ChatWindowHeader, ChatWindowActions, ChatWindowBody, and ChatWindowFooter render a div and accept render. ChatWindowTitle renders an h2. Inside a breadcrumb, wrap only the current page and pass render={<span />}. ChatWindowClose takes label (default "Close").

Accessibility

The window has role="dialog" and is named by ChatWindowTitle. It does not trap focus or hide the page, so people can keep working beside it. When it opens, focus goes to the window itself, so the composer shows no focus ring and no phone keyboard pops up until someone clicks or tabs into it. Pass initialFocus to focus something else. Escape, Minimize, and Close return focus to the launcher. The layout button's name includes the current layout, and every icon button has a tooltip.

A window grows from its corner and fades in over 180ms, and shrinks back in 120ms. A drawer slides 2.5rem from its side. Motion turns off when reduced motion is requested.

Theme tokens

Chat Window uses --sprout-canvas for the window's frame, --sprout-surface, --sprout-line, --sprout-shadow-overlay, and --sprout-radius-panel. Set --sprout-chat-window-width, -height, -drawer-width, -measure, and -inset on :root to change its size and distance from the corner.