AI Panel
ProThe assistant sidepanel: a header, a scrollable conversation and a pinned composer in one column. Drop a Chat in the body and a Prompt Input in the footer. Pure layout, so it docks in a page or drops into a Drawer.
Installation#
Usage#
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
The panel fills its container (h-full) and owns the scroll: only the body scrolls, while the header and composer stay pinned. Give it a sized frame when docking it in a page.
Anatomy#
import {
AIPanel,
AIPanelHeader,
AIPanelHeading,
AIPanelTitle,
AIPanelDescription,
AIPanelActions,
AIPanelAction,
AIPanelExpandToggle,
AIPanelBody,
AIPanelFooter,
} from "@/components/ui/ai-panel"
<AIPanel>
<AIPanelHeader>
<AIPanelHeading>
<AIPanelTitle />
<AIPanelDescription />
</AIPanelHeading>
<AIPanelActions>
<AIPanelExpandToggle />
<AIPanelAction />
</AIPanelActions>
</AIPanelHeader>
<AIPanelBody />
<AIPanelFooter />
</AIPanel>Window modes (split & floating)#
The panel can manage its own window, Notion/Edge-style. Beyond the embedded inline default, it has two windowed views: split (a full-height rail pinned to the right edge, the split-screen assistant) and floating (a compact window hovering bottom-right). Drop an AIPanelExpandToggle in the header to flip between them; it swaps its own icon and label with the view. These modes are non-modal: the page stays interactive, so closing is the host's call (render the panel behind a launcher). The view is controllable via view / defaultView / onViewChange.
Inside an app frame (anchor)#
By default the split and floating windows pin to the browser window (fixed, sized in dvh). When the assistant lives inside an app frame instead (a tablet window, a device mock, a dashboard pane) pass anchor="container": the panel positions itself absolute against the nearest positioned ancestor and sizes the rail and the window in % of it, so it travels and scales with the frame. Give the frame relative overflow-hidden. The AIPanelExpandToggle morphs between the two exactly as it does on the viewport, and the docked rail draws only its inner border.
The example also hands the scroll to the thread: <AIPanelBody scrollable={false}> becomes a fixed column and the Conversation filling it scrolls, so a ConversationScrollButton can float over its bottom edge. Scroll up in the thread to see it.
As an overlay (Drawer)#
Because it's pure layout, the same panel drops straight into a DrawerContent for a slide-in assistant: the Drawer brings the trigger, scrim, focus trap, and animation. Hide the Drawer's own close (showClose=false) and put a close action in the panel header instead. An overlay is never a dead end: keep a minimize to window action that pops the panel out to a floating window (and back), so the assistant can ride along while the user keeps working.
Empty state#
With no messages yet, center a greeting and a few starters in the body. The body is a plain flex region, so a centered welcome composes with no extra parts. For genuine no-content moments (no history, no results, offline), reach for the canonical EmptyState instead: see below.
No content & error states#
Beyond the welcome state, an assistant hits real no-content moments: an empty history, a search that finds nothing, a dropped connection. Because the body is a plain flex region, drop the canonical EmptyState straight in. density="compact" fits the in-panel scale, and variant re-themes the media and action for an error (destructive) versus a neutral zero state (default).
API reference#
AIPanel
The shell: a full-height flex column on the popover surface that owns the view mode. Forwards all native <div> props. Every part accepts className, merged last.
| Prop | Type | Default | Description |
|---|---|---|---|
| view | "inline" | "split" | "floating" | - | The view mode, controlled: fills its container, a full-height right rail, or a bottom-right window. |
| defaultView | "inline" | "split" | "floating" | "inline" | The initial view mode, uncontrolled. |
| onViewChange | (view: "inline" | "split" | "floating") => void | - | Called when the view mode changes, e.g. from AIPanelExpandToggle. |
| anchor | "viewport" | "container" | "viewport" | What the split and floating windows are positioned against: the browser window (fixed), or the nearest positioned ancestor (absolute, sized in %) for a panel inside an app frame. Ignored by inline. |
| Data attribute | Description |
|---|---|
| data-view | "inline", "split" or "floating": the current view mode. |
| data-anchor | "viewport" or "container"; absent in the inline view. |
| CSS variable | Description |
|---|---|
| --surface | Set to var(--popover), so a nested composer blends with the panel rather than the page. |
AIPanelHeader
The bordered header row: leading media (an Avatar), the heading column and the actions. Inside a Drawer, also render a <DrawerTitle className="sr-only">. Forwards all native <div> props.
AIPanelHeading
The title and description column that grows between the media and the actions. Forwards all native <div> props.
AIPanelTitle
The panel title, truncated to one line. Forwards all native <div> props.
AIPanelDescription
A muted one-line subtitle under the title. Forwards all native <div> props.
AIPanelActions
The trailing action row of the header. Forwards all native <div> props.
AIPanelAction
A header action (new chat, history, close): a Button that takes every Button prop. Use asChild to wrap a DrawerClose.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | ButtonProps["variant"] | "ghost" | Any Button variant. |
| size | "sm" | "md" | "lg" | "xl" | "sm" | The Button size. |
| iconOnly | boolean | true | A square icon button; its aria-label doubles as the tooltip. |
AIPanelExpandToggle
A prewired AIPanelAction that flips the panel between the split rail and the floating window, swapping its icon and label ("Float" or "Dock to side"). Takes every Button prop; must live inside an AIPanel.
AIPanelBody
The only scrolling region, with edge fades; drop a Conversation inside. Forwards all native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| scrollable | boolean | true | The body scrolls its own overflow, padded with a scroll fade. Set false to hand the scroll to the child that fills it, e.g. a Conversation with min-h-0 flex-1, so a ConversationScrollButton can float over the thread. |
| Data attribute | Description |
|---|---|
| data-scrollable | "false" when scrollable is off; absent otherwise. |
AIPanelFooter
The pinned composer dock; drop a PromptInput inside. Forwards all native <div> props.