Elements

Assistant modal

A floating chat bubble for support widgets, help desks, and embedded assistants, with a thread list and a resizable window.

fig. 01

Installation

npx shadcn@latest add "@assistant-ui/assistant-modal"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

Assistant modal tucks a complete Thread behind a floating corner trigger: a bot icon that becomes a chevron once open, above a popover that holds the conversation. Its header names the current thread, opens the thread list from a toggle, and starts a new thread from the plus button, and the window resizes from its top corner. It has no standalone form, since it is a fixed composition of Thread and a runtime-driven open state rather than a set of props.

Getting started

AssistantModal needs nothing beyond a runtime provider higher up the tree; it positions and opens itself.

Drop it in

app/layout.tsx
import { AssistantModal } from "@/components/assistant-ui/elements/assistant-modal.aui";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      {children}
      <AssistantModal />
    </>
  );
}

Needs an AssistantRuntimeProvider ancestor. Render it once, anywhere under the provider, since it is fixed-positioned to the viewport corner on its own.

Anatomy

<div className="aui-modal-anchor fixed end-4 bottom-4 size-11">
  <button aria-label="Open Assistant">{/* bot icon, crossfades to a chevron once open */}</button>
</div>

<div className="aui-modal-content"> {/* opens above the trigger */}
  <button aria-label="Resize Assistant" className="aui-modal-resize-handle" /> {/* the top start corner */}
  <div className="aui-modal-header">
    <h2 className="aui-modal-title">{/* the thread title, or Threads in the list */}</h2>
    <button aria-label="Threads" className="aui-modal-threads" /> {/* history icon, pressed while the list shows */}
    <button aria-label="New Thread" className="aui-modal-new" /> {/* plus icon */}
  </div>
  <div className="aui-modal-body">
    <div className="aui-modal-thread">
      <Thread />
    </div>
    <ThreadListRoot className="aui-modal-thread-list" /> {/* shown after Threads is pressed */}
  </div>
</div>

The modal opens itself on the runtime's thread.runStart event, so sending the first message expands it even if it was launched collapsed. Once open, clicking outside or moving focus away leaves it open; clicking the trigger again or pressing Escape closes it. The trigger's accessible label switches between "Open Assistant" and "Close Assistant" along with the icon crossfade.

The Threads toggle swaps the conversation for the thread list, with search, and is disabled until a thread exists; pressing it again, picking a thread, or starting a new one returns to the conversation, and every open starts on the conversation. The title stays in the same place in both views. Drag the top start corner, or focus it and use the arrow keys (Shift moves four times as far), to resize. The size is clamped to the viewport, stored in localStorage under aui-modal-size, and a double click on the corner, or Enter while it has focus, restores the default.

Examples

Trigger it from elsewhere

AssistantModal owns its open state internally and exposes no open prop or ref. To open it from a different control, such as a header button, lift the open/setOpen pair already at the top of the component into your own copy of the file.

Restyle the trigger and panel

The anchor, panel, and button each carry a stable class name you can target from outside without touching the copied file: aui-modal-anchor positions the trigger, aui-modal-content sizes and animates the panel, and aui-modal-button styles the trigger itself.

.aui-modal-content {
  width: 28rem;
  height: 36rem;
}

A width and height set this way are the default size; once someone resizes the window, the stored size is applied inline and takes precedence.

API reference

AssistantModal

Takes no props.

Behavior

EventFiresEffect
thread.runStartOnce per run start, any threadOpens the modal on the conversation.

Class names

ClassTargets
aui-modal-anchorThe fixed-position trigger wrapper.
aui-modal-contentThe popover panel that holds Thread.
aui-modal-buttonThe floating trigger button.
aui-modal-headerThe header row.
aui-modal-threadsThe Threads toggle that shows and hides the thread list.
aui-modal-titleThe heading: the thread title, or Threads in the list.
aui-modal-newThe New Thread button.
aui-modal-bodyThe area below the header.
aui-modal-threadThe wrapper around Thread.
aui-modal-thread-listThe thread list shown in place of the conversation.
aui-modal-resize-handleThe resizable top start corner.