Composer

Build custom message input UIs with full control over layout and behavior.

The Composer primitive is the interface for composing new messages or editing existing ones. It handles submit behavior, keyboard shortcuts, focus management, attachment state, and streaming status. You provide the UI.

Quick Start

Minimal example:

import { ComposerPrimitive } from "@assistant-ui/react";

<ComposerPrimitive.Root>
  <ComposerPrimitive.Input placeholder="Ask anything..." />
  <ComposerPrimitive.Send>Send</ComposerPrimitive.Send>
</ComposerPrimitive.Root>

Root renders a <form>, Input renders a <textarea>, and Send renders a <button>. Each part renders its native element by default, and supports asChild for element substitution.

Runtime setup: primitives require runtime context. Wrap your UI in AssistantRuntimeProvider with a runtime (for example useLocalRuntime(...)). See Pick a Runtime.

Core Concepts

New Message vs Edit Mode

A Composer placed inside a Thread composes new messages. A Composer placed inside a Message edits that message. The same primitives handle both, and the behavior changes automatically based on context.

// New message composer
<ThreadPrimitive.Root>
  <ComposerPrimitive.Root>
    <ComposerPrimitive.Input />
    <ComposerPrimitive.Send>Send</ComposerPrimitive.Send>
  </ComposerPrimitive.Root>
</ThreadPrimitive.Root>

// Edit composer (inside a message)
<MessagePrimitive.Root>
  <ComposerPrimitive.Root>
    <ComposerPrimitive.Input />
    <ComposerPrimitive.Send>Save</ComposerPrimitive.Send>
    <ComposerPrimitive.Cancel>Cancel</ComposerPrimitive.Cancel>
  </ComposerPrimitive.Root>
</MessagePrimitive.Root>

The asChild Pattern

Every primitive part accepts asChild to merge its behavior onto your own element. This is how you use primitives with your own design system:

import { ComposerPrimitive } from "@assistant-ui/react";

<ComposerPrimitive.Input asChild>
  <textarea
    className="my-custom-textarea"
    placeholder="Type here..."
  />
</ComposerPrimitive.Input>

<ComposerPrimitive.Send asChild>
  <MyButton variant="primary">Send</MyButton>
</ComposerPrimitive.Send>

The primitive's behavior (keyboard handling, disabled state, form submission) is merged onto your element. Your styles, your component, primitive wiring.

Unstable Mentions

Composer also includes an unstable mention system for @-triggered popovers. It is built around ComposerPrimitive.Unstable_MentionRoot and intended for rich inputs like LexicalComposerInput.

<ComposerPrimitive.Unstable_MentionRoot>
  <ComposerPrimitive.Root>
    <LexicalComposerInput placeholder="Type @ to mention a tool..." />
    <ComposerPrimitive.Unstable_MentionPopover />
  </ComposerPrimitive.Root>
</ComposerPrimitive.Unstable_MentionRoot>

Parts

Root

Form container for message composition. Renders a <form> element unless asChild is set.

<ComposerPrimitive.Root className="flex w-full flex-col rounded-3xl border bg-muted">
  <ComposerPrimitive.Input placeholder="Ask anything..." />
  <ComposerPrimitive.Send>Send</ComposerPrimitive.Send>
</ComposerPrimitive.Root>

Input

Text input with keyboard shortcuts. Renders a <textarea> element unless asChild is set.

<ComposerPrimitive.Input
  submitMode="ctrlEnter"
  cancelOnEscape
  placeholder="Ask anything..."
/>

Prop

Type

Send

Submits the composer form. Renders a <button> element unless asChild is set.

<ComposerPrimitive.Send className="rounded-full bg-primary px-3 py-2 text-primary-foreground">
  Send
</ComposerPrimitive.Send>

Cancel

Cancels the current composition or edit session. Renders a <button> element unless asChild is set.

<ComposerPrimitive.Cancel className="rounded-md px-3 py-2 text-sm hover:bg-muted">
  Cancel
</ComposerPrimitive.Cancel>

AddAttachment

Opens file picker for attachments. Renders a <button> element unless asChild is set.

<ComposerPrimitive.AddAttachment multiple>
  Attach Files
</ComposerPrimitive.AddAttachment>

Prop

Type

Attachments

Renders each attachment. Prefer the children render function for new code.

<ComposerPrimitive.Attachments>
  {({ attachment }) => {
    if (attachment.type === "image") return <ImagePreview />;
    if (attachment.type === "document") return <DocumentPreview />;
    return <GenericPreview />;
  }}
</ComposerPrimitive.Attachments>

Prop

Type

AttachmentByIndex

Renders a single attachment at a specific index.

<ComposerPrimitive.AttachmentByIndex
  index={0}
  components={{ Attachment: MyAttachment }}
/>

Prop

Type

index
number
components?
ComposerAttachmentsComponentConfig

AttachmentDropzone

Drag-and-drop zone for file attachments. Sets data-dragging when a file is being dragged over it. Renders a <div> element unless asChild is set.

<ComposerPrimitive.AttachmentDropzone className="rounded-xl border-2 border-dashed data-[dragging]:border-primary data-[dragging]:bg-primary/5">
  <ComposerPrimitive.Root>
    ...
  </ComposerPrimitive.Root>
</ComposerPrimitive.AttachmentDropzone>

Prop

Type

Dictate

Starts a dictation session. Renders a <button> element unless asChild is set.

<ComposerPrimitive.Dictate className="rounded-md px-3 py-2 text-sm hover:bg-muted">
  Start Dictation
</ComposerPrimitive.Dictate>

StopDictation

Stops the current dictation session. Renders a <button> element unless asChild is set.

<ComposerPrimitive.StopDictation className="rounded-md px-3 py-2 text-sm hover:bg-muted">
  Stop Dictation
</ComposerPrimitive.StopDictation>

DictationTranscript

Renders the interim transcript while dictation is active. Renders a <span> element unless asChild is set.

<ComposerPrimitive.DictationTranscript className="text-sm text-muted-foreground" />

If (deprecated)

Deprecated. Use AuiIf instead.

// Before (deprecated)
<ComposerPrimitive.If editing>...</ComposerPrimitive.If>
<ComposerPrimitive.If dictation>...</ComposerPrimitive.If>

// After
<AuiIf condition={(s) => s.composer.isEditing}>...</AuiIf>
<AuiIf condition={(s) => s.composer.dictation != null}>...</AuiIf>

Quote

Container for quoted text preview inside the composer. Only renders when a quote is set. Renders a <div> element unless asChild is set.

<ComposerPrimitive.Quote className="rounded-lg border bg-background px-3 py-2 text-sm">
  <ComposerPrimitive.QuoteText />
  <ComposerPrimitive.QuoteDismiss className="ml-2" />
</ComposerPrimitive.Quote>

QuoteText

Renders the quoted text content. Renders a <span> element unless asChild is set.

<ComposerPrimitive.QuoteText />

QuoteDismiss

Clears the active quote from the composer. Renders a <button> element unless asChild is set.

<ComposerPrimitive.QuoteDismiss className="rounded-full p-1 hover:bg-muted">
  Dismiss
</ComposerPrimitive.QuoteDismiss>

Unstable_MentionRoot

Provider that manages mention state and @ trigger detection.

<ComposerPrimitive.Unstable_MentionRoot trigger="@" adapter={mentionAdapter}>
  <ComposerPrimitive.Root>
    <LexicalComposerInput placeholder="Type @ to mention a tool..." />
    <ComposerPrimitive.Unstable_MentionPopover />
  </ComposerPrimitive.Root>
</ComposerPrimitive.Unstable_MentionRoot>

Prop

Type

children
React.ReactNode

Unstable_MentionPopover

Container for the mention picker popover. It only renders while a trigger match is active.

<ComposerPrimitive.Unstable_MentionPopover className="rounded-lg border bg-popover p-1 shadow-md">
  <ComposerPrimitive.Unstable_MentionCategories>
    {(categories) => categories.map((category) => (
      <ComposerPrimitive.Unstable_MentionCategoryItem
        key={category.id}
        categoryId={category.id}
      >
        {category.label}
      </ComposerPrimitive.Unstable_MentionCategoryItem>
    ))}
  </ComposerPrimitive.Unstable_MentionCategories>
</ComposerPrimitive.Unstable_MentionPopover>

Unstable_MentionCategories

Render-function primitive for the top-level mention categories.

<ComposerPrimitive.Unstable_MentionCategories>
  {(categories) => categories.map((category) => (
    <ComposerPrimitive.Unstable_MentionCategoryItem
      key={category.id}
      categoryId={category.id}
    >
      {category.label}
    </ComposerPrimitive.Unstable_MentionCategoryItem>
  ))}
</ComposerPrimitive.Unstable_MentionCategories>

Prop

Type

Unstable_MentionCategoryItem

Button that selects a mention category and drills into its items.

<ComposerPrimitive.Unstable_MentionCategoryItem categoryId="tools">
  Tools
</ComposerPrimitive.Unstable_MentionCategoryItem>

Unstable_MentionItems

Render-function primitive for the items inside the currently selected category.

<ComposerPrimitive.Unstable_MentionItems>
  {(items) => items.map((item) => (
    <ComposerPrimitive.Unstable_MentionItem key={item.id} item={item} />
  ))}
</ComposerPrimitive.Unstable_MentionItems>

Prop

Type

Unstable_MentionItem

Selectable mention item inside the popover.

<ComposerPrimitive.Unstable_MentionItem item={item}>
  {item.label}
</ComposerPrimitive.Unstable_MentionItem>

Prop

Type

item
Unstable_MentionItem

Unstable_MentionBack

Back button used when drilling from categories into a specific item list.

<ComposerPrimitive.Unstable_MentionBack className="rounded-md px-2 py-1 text-sm hover:bg-accent">
  Back
</ComposerPrimitive.Unstable_MentionBack>

Patterns

With Attachments

<ComposerPrimitive.Root>
  <ComposerPrimitive.Attachments>
    {({ attachment }) => {
      if (attachment.type === "image") return <ImagePreview />;
      if (attachment.type === "document") return <DocumentPreview />;
      return <GenericPreview />;
    }}
  </ComposerPrimitive.Attachments>
  <ComposerPrimitive.Input placeholder="Ask anything..." />
  <ComposerPrimitive.AddAttachment>
    Attach
  </ComposerPrimitive.AddAttachment>
  <ComposerPrimitive.Send>Send</ComposerPrimitive.Send>
</ComposerPrimitive.Root>

With Drag-and-Drop

Wrap your composer with AttachmentDropzone to support dragging files directly onto the input area:

<ComposerPrimitive.AttachmentDropzone className="rounded-xl border-2 border-dashed data-[dragging]:border-primary data-[dragging]:bg-primary/5">
  <ComposerPrimitive.Root>
    <ComposerPrimitive.Attachments>
      {() => <MyAttachment />}
    </ComposerPrimitive.Attachments>
    <ComposerPrimitive.Input placeholder="Drop files or type..." />
    <ComposerPrimitive.AddAttachment>Attach</ComposerPrimitive.AddAttachment>
    <ComposerPrimitive.Send>Send</ComposerPrimitive.Send>
  </ComposerPrimitive.Root>
</ComposerPrimitive.AttachmentDropzone>

The dropzone sets data-dragging when a file is being dragged over it, so you can style the active state with CSS.

With Voice Input

<ComposerPrimitive.Root>
  <ComposerPrimitive.Input placeholder="Ask or speak..." />
  <AuiIf condition={(s) => s.composer.dictation != null}>
    <ComposerPrimitive.DictationTranscript className="text-sm text-muted-foreground" />
  </AuiIf>
  <AuiIf condition={(s) => s.composer.dictation == null}>
    <ComposerPrimitive.Dictate>Mic</ComposerPrimitive.Dictate>
  </AuiIf>
  <AuiIf condition={(s) => s.composer.dictation != null}>
    <ComposerPrimitive.StopDictation>Stop</ComposerPrimitive.StopDictation>
  </AuiIf>
  <ComposerPrimitive.Send>Send</ComposerPrimitive.Send>
</ComposerPrimitive.Root>

DictationTranscript renders a <span> showing the interim speech-to-text transcript while the user is speaking.

Voice input requires a DictationAdapter configured in your runtime. See Speech & Dictation for setup.

Custom Submit Behavior

Use onSubmit on Root to intercept submission:

<ComposerPrimitive.Root
  onSubmit={(e) => {
    // Runs before the message is sent
    // e.g., track analytics, transform input
    // Call e.preventDefault() to cancel the send
  }}
>
  <ComposerPrimitive.Input />
  <ComposerPrimitive.Send>Send</ComposerPrimitive.Send>
</ComposerPrimitive.Root>

Ctrl+Enter to Submit

<ComposerPrimitive.Root>
  <ComposerPrimitive.Input submitMode="ctrlEnter" />
  <ComposerPrimitive.Send>Send</ComposerPrimitive.Send>
</ComposerPrimitive.Root>

With submitMode="ctrlEnter", plain Enter inserts a newline and Ctrl/Cmd+Enter submits.

Floating Composer

Primitives aren't bound to any layout. Here's how the floating composer on this docs site is built, using the same ComposerPrimitive.Root and ComposerPrimitive.Input positioned with CSS:

import { ComposerPrimitive } from "@assistant-ui/react";

function FloatingComposer() {
  return (
    <div className="fixed bottom-6 left-1/2 z-40 w-full max-w-md -translate-x-1/2">
      <ComposerPrimitive.Root>
        <div className="rounded-xl border bg-background/80 shadow-lg backdrop-blur-sm">
          <ComposerPrimitive.Input
            asChild
            unstable_focusOnRunStart={false}
            unstable_focusOnScrollToBottom={false}
          >
            <textarea
              placeholder="Ask a question..."
              className="w-full resize-none bg-transparent px-3 py-2.5 text-sm focus:outline-none"
              rows={1}
            />
          </ComposerPrimitive.Input>
        </div>
      </ComposerPrimitive.Root>
    </div>
  );
}

The primitive handles everything about composing a message. The layout, animation, and visibility logic is all yours.

Relationship to Components

The Thread component includes a full composer built from these primitives. If you need a working chat UI fast, start there. When you need a composer that doesn't fit inside a thread, such as a floating input, a sidebar composer, or a multi-step form, reach for ComposerPrimitive directly.

API Reference

For full prop details on every part, see the ComposerPrimitive API Reference.

Related: