Elements

Composer

The unified input: attachments, commands, mentions, models, voice, and context in one surface.

screenshot.png128 KB

Context

37%

System12k
Tools8k
Messages54k
Total74k / 200k
fig. 01

Installation

npx shadcn@latest add "@assistant-ui/elements-composer"
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.

The composer is the rounded bar where a person writes the next message: a growing text field with a toolbar underneath for attachments, the active model, and a send button that swaps to a stop button mid-run. With a runtime the bar reads and writes the thread's live composer state; standalone you hold that state yourself.

Getting started

Every assistant-ui runtime carries a composer scoped to the active thread (or to a message being edited): a place to hold the draft text, staged attachments, and the send and cancel actions. Build the bar from ComposerPrimitive, the same primitives the Thread element renders internally, and pair them with the shared surface tokens from surfaces.tsx to match this catalog's look.

Compose the bar

components/assistant-ui/elements/composer-bar.tsx
"use client";

import { AuiIf, ComposerPrimitive } from "@assistant-ui/react";
import { ArrowUpIcon, PlusIcon, SquareIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { ghostButton, inkButton, paper } from "@/components/assistant-ui/elements/surfaces";

export function ComposerBar() {
  return (
    <ComposerPrimitive.Root className={cn(paper, "flex w-full max-w-lg flex-col gap-2 rounded-[24px] p-2.5")}>
      <ComposerPrimitive.Input
        placeholder="Message..."
        rows={1}
        className="placeholder:text-foreground/35 min-h-11 w-full resize-none bg-transparent px-3 text-[15px] outline-none"
      />
      <div className="flex items-center justify-between">
        <ComposerPrimitive.AddAttachment
          aria-label="Add attachment"
          className={cn(ghostButton, "size-8 disabled:pointer-events-none disabled:opacity-30")}
        >
          <PlusIcon className="size-4" />
        </ComposerPrimitive.AddAttachment>
        <AuiIf condition={(s) => !s.thread.isRunning}>
          <ComposerPrimitive.Send
            aria-label="Send message"
            className={cn(inkButton, "grid size-8 place-items-center rounded-full")}
          >
            <ArrowUpIcon className="size-4" />
          </ComposerPrimitive.Send>
        </AuiIf>
        <AuiIf condition={(s) => s.thread.isRunning}>
          <ComposerPrimitive.Cancel
            aria-label="Stop generating"
            className={cn(inkButton, "grid size-8 place-items-center rounded-full")}
          >
            <SquareIcon className="size-3 fill-current" />
          </ComposerPrimitive.Cancel>
        </AuiIf>
      </div>
    </ComposerPrimitive.Root>
  );
}

ComposerPrimitive.Root renders a <form> that submits on Enter (Shift+Enter for a newline) and sends whatever text and attachments are staged. AuiIf swaps Send for Cancel the moment the thread starts running, the same swap Thread's own composer makes.

Add the rest as you need it

Attachments, the / and @ menus, the model trigger, and dictation are each their own primitives layered onto this same bar. See Attachments, Slash commands, Mentions, Models, and Dictation for each piece's own wiring. The Thread element ships a complete composer built from these same primitives, so installing Thread gives you a working bar without assembling one yourself; this element is for building your own bar, or for a surface Thread does not cover, such as a compact inline composer.

Anatomy

<div data-slot="composer">
  <div data-slot="composer-bar" data-drag-active={/* true while a file is dragged over */}>
    <div data-slot="composer-attachments">{/* staged files, when any */}</div>
    <input data-slot="composer-input" />
    <div data-slot="composer-toolbar">
      <button data-slot="composer-attach" />
      <div data-slot="composer-actions">
        {/* model trigger, voice button, context ring, ... */}
        <button data-slot="composer-send" />
      </div>
    </div>
  </div>
</div>

ComposerBar is the only piece with real visual state of its own: dragActive tints it and rounds its corners into a drop target. Every other piece here is a plain, unstyled-by-default building block; the composition (what sits in the toolbar, whether the model trigger or voice button appears) is up to the page that assembles them.

Examples

Swapping send for cancel

AuiIf mounts exactly one of the two, matching how Thread's own composer switches them; a runtime with the queue capability enabled would still let a send reach the queue while running, so that composition unmounts Send a beat before the button's own canSend logic would otherwise have kept it live:

<ComposerActions>
  <AuiIf condition={(s) => !s.thread.isRunning}>
    <ComposerPrimitive.Send className={cn(inkButton, "grid size-8 place-items-center rounded-full")}>
      <ArrowUpIcon className="size-4" />
    </ComposerPrimitive.Send>
  </AuiIf>
  <AuiIf condition={(s) => s.thread.isRunning}>
    <ComposerPrimitive.Cancel className={cn(inkButton, "grid size-8 place-items-center rounded-full")}>
      <SquareIcon className="size-3 fill-current" />
    </ComposerPrimitive.Cancel>
  </AuiIf>
</ComposerActions>

Restyle the bar

Every piece here takes className, and the rounded corners, borders, and hover states all come from the shared paper, ghostButton, and inkButton tokens in surfaces.tsx. Restyling those tokens restyles every element in the catalog that uses them, not just the composer.

<ComposerBar className="max-w-2xl rounded-2xl p-4" />

API reference

ComposerPrimitive

PartRendersNotes
RootformSends on submit, which Input triggers on Enter; compact collapses it to a single row while the text holds at most one line and there are no attachments, quote, queued messages, or active dictation.
Inputauto-resizing textareaControlled by the runtime; disabled while the thread is disabled or dictation is active. submitMode ("enter" | "ctrlEnter" | "none") controls Enter's behavior.
SendbuttonDisabled while !s.composer.canSend, and also while the thread is running unless it has the queue capability.
CancelbuttonDisabled while !s.composer.canCancel.
AddAttachmentbuttonOpens a native file picker filtered by attachmentAccept; disabled only while the composer is not editable, so a pick made with no adapter configured opens the picker but silently fails to add the file.
AttachmentDropzonediv (or asChild)Sets data-dragging="true" while a file is dragged over; drops call addAttachment per file.

Composer state

SelectorTypeDescription
s.composer.textstringThe current draft text.
s.composer.isEmptybooleanWhether the composer has no text and no attachments.
s.composer.canSendbooleanWhether send() would do anything right now.
s.composer.canCancelbooleanWhether cancel() would do anything right now.
s.composer.attachmentsreadonly Attachment[]Files staged on this composer; see Attachments.
s.thread.isRunningbooleanWhether the thread has a run in flight.
aui.composer.send(options?)(options?: { startRun?: boolean; steer?: boolean }) => voidSends the staged text and attachments.
aui.composer.cancel()() => voidCancels the current run, or exits edit mode.