Assistant modal
A floating chat bubble for support widgets, help desks, and embedded assistants, with a thread list and a resizable window.
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 initThen 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.
This component is composed from runtime primitives and has no standalone build.
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
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.
Assistant modal has no standalone form: it composes the runtime-only Thread inside a popover it opens itself, tied to the runtime's thread.runStart event. Build your own floating trigger around a popover and Thread's primitives if you need different open behavior.
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
| Event | Fires | Effect |
|---|---|---|
thread.runStart | Once per run start, any thread | Opens the modal on the conversation. |
Class names
| Class | Targets |
|---|---|
aui-modal-anchor | The fixed-position trigger wrapper. |
aui-modal-content | The popover panel that holds Thread. |
aui-modal-button | The floating trigger button. |
aui-modal-header | The header row. |
aui-modal-threads | The Threads toggle that shows and hides the thread list. |
aui-modal-title | The heading: the thread title, or Threads in the list. |
aui-modal-new | The New Thread button. |
aui-modal-body | The area below the header. |
aui-modal-thread | The wrapper around Thread. |
aui-modal-thread-list | The thread list shown in place of the conversation. |
aui-modal-resize-handle | The resizable top start corner. |