Radix UI Primitives

ComposerPrimitive

Primitives for the text input, send button, and attachments.

The user interface to add new messages or edit existing ones.

Dual Use! A Composer placed directly inside a Thread will compose new messages. A Composer placed inside a Message will edit that message.

Anatomy

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

// creating a new message
const Composer = () => (
  <ComposerPrimitive.Root>
    <ComposerPrimitive.AttachmentDropzone>
      <ComposerPrimitive.Quote>
        <ComposerPrimitive.QuoteText />
        <ComposerPrimitive.QuoteDismiss />
      </ComposerPrimitive.Quote>
      <ComposerPrimitive.Attachments />
      <ComposerPrimitive.AddAttachment />
      <ComposerPrimitive.Input />
      <ComposerPrimitive.Send />
    </ComposerPrimitive.AttachmentDropzone>
  </ComposerPrimitive.Root>
);

// editing an existing message
const EditComposer = () => (
  <ComposerPrimitive.Root>
    <ComposerPrimitive.Input />
    <ComposerPrimitive.Send />
    <ComposerPrimitive.Cancel />
  </ComposerPrimitive.Root>
);

// with voice input (dictation)
const ComposerWithDictation = () => (
  <ComposerPrimitive.Root>
    <ComposerPrimitive.Input />
    <AuiIf condition={(s) => s.composer.dictation == null}>
      <ComposerPrimitive.Dictate />
    </AuiIf>
    <AuiIf condition={(s) => s.composer.dictation != null}>
      <ComposerPrimitive.StopDictation />
    </AuiIf>
    <ComposerPrimitive.Send />
  </ComposerPrimitive.Root>
);

API Reference

Root

Contains all parts of the composer.

This primitive renders a <form> element unless asChild is set.

ComposerRootProps
asChild: boolean= false

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

Input

The text input field for the user to type a new message.

This primitive renders a <textarea> element unless asChild is set.

ComposerPrimitiveInputProps
asChild: boolean= false

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

submitMode: "enter" | "ctrlEnter" | "none"= "enter"

Controls how the Enter key submits messages. "enter": plain Enter submits (Shift+Enter for newline). "ctrlEnter": Ctrl/Cmd+Enter submits (plain Enter for newline). "none": keyboard submission disabled.

cancelOnEscape: boolean= true

Whether to cancel message composition when Escape is pressed.

unstable_focusOnRunStartunstable: boolean= true

Whether to automatically focus the input when a new run starts.

unstable_focusOnScrollToBottomunstable: boolean= true

Whether to automatically focus the input when scrolling to bottom.

unstable_focusOnThreadSwitchedunstable: boolean= true

Whether to automatically focus the input when switching threads.

addAttachmentOnPaste: boolean= true

Whether to automatically add pasted files as attachments.

Keyboard Shortcuts

Default (submitMode="enter"):

KeyDescription
EnterSends the message.
Shift + EnterInserts a newline.
EscapeSends a cancel action.

With submitMode="ctrlEnter":

KeyDescription
Ctrl/Cmd + EnterSends the message.
EnterInserts a newline.
EscapeSends a cancel action.

Send

The button to send the message.

This primitive renders a <button> element unless asChild is set.

ComposerPrimitiveSendProps
asChild: boolean= false

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

Cancel

Sends a cancel action.

In edit composers, this action exits the edit mode. In thread composers, this action stops the current run.

This primitive renders a <button> element unless asChild is set.

ComposerPrimitiveCancelProps
asChild: boolean= false

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

Attachments

Renders attachments. This primitive renders a separate component for each attachment.

ComposerPrimitiveAttachmentsProps
components?: ComposerAttachmentsComponents

The component to render for each attachment.

ComposerPrimitiveAttachmentsProps['components']
Image?: ComponentType

The component to render for each image attachment.

Document?: ComponentType

The component to render for each document attachment.

File?: ComponentType

The component to render for each file attachment.

Attachment?: ComponentType

The fallback component to render for each attachment type.

AttachmentByIndex

Renders a single attachment at the specified index within the composer.

<ComposerPrimitive.AttachmentByIndex
  index={0}
  components={{
    Image: MyImageAttachment,
    Document: MyDocumentAttachment
  }}
/>
ComposerPrimitive.AttachmentByIndex.Props
index: number

The index of the attachment to render.

components?: ComposerAttachmentsComponents

The components to render for the attachment.

AddAttachment

Renders a button to add an attachment.

This primitive renders a <button> element unless asChild is set.

ComposerPrimitiveAddAttachmentProps
asChild: boolean= false

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

multiple: boolean | undefined= true

Allow selecting multiple attachments at the same time.

AttachmentDropzone

A drag-and-drop zone that accepts file drops and adds them as attachments to the composer.

When a file is dragged over the zone, a data-dragging="true" attribute is set on the element, which can be used for styling the active drag state.

This primitive renders a <div> element unless asChild is set.

<ComposerPrimitive.AttachmentDropzone className="relative data-[dragging]:ring-2">
  <ComposerPrimitive.Input />
  <ComposerPrimitive.Send />
</ComposerPrimitive.AttachmentDropzone>
ComposerPrimitiveAttachmentDropzoneProps
asChild: boolean= false

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

disabled?: boolean | undefined

When true, drag-and-drop is disabled and files will not be added on drop.

Dictate

Renders a button to start dictation to convert voice to text.

Requires a DictationAdapter to be configured in the runtime.

This primitive renders a <button> element unless asChild is set.

ComposerPrimitiveDictateProps
asChild: boolean= false

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

StopDictation

Renders a button to stop the current dictation session.

Only rendered when dictation is active.

This primitive renders a <button> element unless asChild is set.

ComposerPrimitiveStopDictationProps
asChild: boolean= false

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

DictationTranscript

Renders the current interim (partial) transcript while dictation is active.

Note: By default, interim transcripts are displayed directly in the composer input (like native dictation). This component is for advanced customization when you want to display the interim transcript separately (e.g., in a different style or location).

Only renders when there is an active interim transcript (returns null otherwise).

This primitive renders a <span> element.

{/* Optional: Display interim transcript separately with custom styling */}
<AuiIf condition={(s) => s.composer.dictation != null}>
  <div className="dictation-preview">
    <ComposerPrimitive.DictationTranscript className="italic text-muted" />
  </div>
</AuiIf>

Quote

A container for displaying a quote preview in the composer. Only renders when a quote is set via composer.setQuote().

This primitive renders a <div> element.

<ComposerPrimitive.Quote className="flex items-start gap-2 bg-muted/60 px-3 py-2">
  <ComposerPrimitive.QuoteText className="line-clamp-2 flex-1 text-sm" />
  <ComposerPrimitive.QuoteDismiss>×</ComposerPrimitive.QuoteDismiss>
</ComposerPrimitive.Quote>

QuoteText

Renders the quoted text content. Only renders when a quote is set.

This primitive renders a <span> element.

QuoteDismiss

A button that clears the current quote from the composer by calling setQuote(undefined).

This primitive renders a <button> element unless asChild is set.

ComposerPrimitiveQuoteDismissProps
asChild: boolean= false

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

See the Quoting guide for a complete walkthrough including the floating selection toolbar and backend handling.

Conditional Rendering

Use AuiIf for conditional rendering based on composer state:

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

<AuiIf condition={(s) => s.composer.isEditing}>
  {/* rendered if message is being edited */}
</AuiIf>

<AuiIf condition={(s) => s.composer.dictation != null}>
  {/* rendered if dictation is active */}
</AuiIf>

Trigger Popover Primitives (Unstable)

These primitives are under the Unstable_ prefix and may change without notice.

Unified primitives for any character-triggered popover (@ mentions, / slash commands, : emoji, etc.). Multiple triggers coexist under a single TriggerPopoverRoot. See the Mentions guide and Slash Commands guide for full documentation and the ComposerTriggerPopover component for a pre-built implementation.

Anatomy

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

const Composer = () => (
  <ComposerPrimitive.Unstable_TriggerPopoverRoot>
    <ComposerPrimitive.Root>
      <ComposerPrimitive.Input />

      <ComposerPrimitive.Unstable_TriggerPopover
        char="@"
        adapter={mention.adapter}
      >
        <ComposerPrimitive.Unstable_TriggerPopover.Directive
          {...mention.directive}
        />
        <ComposerPrimitive.Unstable_TriggerPopoverCategories>
          {(categories) =>
            categories.map((cat) => (
              <ComposerPrimitive.Unstable_TriggerPopoverCategoryItem
                key={cat.id}
                categoryId={cat.id}
              >
                {cat.label}
              </ComposerPrimitive.Unstable_TriggerPopoverCategoryItem>
            ))
          }
        </ComposerPrimitive.Unstable_TriggerPopoverCategories>
        <ComposerPrimitive.Unstable_TriggerPopoverItems>
          {(items) =>
            items.map((item) => (
              <ComposerPrimitive.Unstable_TriggerPopoverItem
                key={item.id}
                item={item}
              >
                {item.label}
              </ComposerPrimitive.Unstable_TriggerPopoverItem>
            ))
          }
        </ComposerPrimitive.Unstable_TriggerPopoverItems>
        <ComposerPrimitive.Unstable_TriggerPopoverBack>
          Back
        </ComposerPrimitive.Unstable_TriggerPopoverBack>
      </ComposerPrimitive.Unstable_TriggerPopover>
    </ComposerPrimitive.Root>
  </ComposerPrimitive.Unstable_TriggerPopoverRoot>
);

Unstable_TriggerPopoverRoot

Root provider that groups one or more TriggerPopover declarations. Owns the shared ComposerInputPluginRegistry that routes cursor and keyboard events to each registered trigger.

No props.

Unstable_TriggerPopover

Declares a trigger and renders its popover container. Only renders its DOM (and children) while this trigger is active in the composer input. Each declaration has an isolated scope — sub-primitives placed inside read state from this declaration only.

PropTypeDefaultDescription
charstringCharacter(s) that activate the trigger (e.g. "@", "/") — unique within the root
adapterUnstable_TriggerAdapterProvides categories, items, and search

Selection behavior is declared by rendering exactly one behavior sub-primitive inside the TriggerPopover:

Renders a <div> with role="listbox" when open.

Unstable_TriggerPopover.Directive

Behavior sub-primitive. Placed inside a <Unstable_TriggerPopover>. Renders nothing — registers an "directive" behavior with its parent. Exactly one behavior sub-primitive is allowed per TriggerPopover.

PropTypeDefaultDescription
formatterUnstable_DirectiveFormatterunstable_defaultDirectiveFormatterSerializes the selected item into composer text (and parses it back).
onInserted(item) => voidFires after the directive text has been inserted.

Unstable_TriggerPopover.Action

Behavior sub-primitive. Placed inside a <Unstable_TriggerPopover>. Renders nothing — registers an "action" behavior with its parent. Exactly one behavior sub-primitive is allowed per TriggerPopover.

PropTypeDefaultDescription
formatterUnstable_DirectiveFormatterunstable_defaultDirectiveFormatterSerializes the audit-trail chip (unused when removeOnExecute is true).
onExecute(item) => voidCalled the moment an item is selected — typically routes to a handler map. Required.
removeOnExecutebooleanfalseWhen true, strips the trigger text from the composer instead of leaving a chip.

Unstable_TriggerBehavior

Union type returned by the two behavior sub-primitives and consumed internally by the popover resources. Exported for advanced use cases (e.g. custom sub-primitives).

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

// Equivalent shape:
// Unstable_TriggerBehavior =
//   | { kind: "directive"; formatter: Unstable_DirectiveFormatter; onInserted?: (item) => void }
//   | { kind: "action"; formatter: Unstable_DirectiveFormatter; onExecute: (item) => void; removeOnExecute?: boolean }

Unstable_TriggerPopoverCategories

Renders the top-level category list. Accepts a render function (categories) => ReactNode. Hidden when a category is selected or when in search mode.

Unstable_TriggerPopoverCategoryItem

A button that drills into a category. Renders role="option" with automatic data-highlighted and aria-selected when keyboard-navigated.

PropTypeDescription
categoryIdstringThe category to select on click

Unstable_TriggerPopoverItems

Renders the item list for the active category or search results. Accepts a render function (items) => ReactNode. Hidden when no category is selected and not in search mode.

Unstable_TriggerPopoverItem

A button that selects an item. Renders role="option" with automatic data-highlighted and aria-selected when keyboard-navigated.

PropTypeDescription
itemUnstable_TriggerItemThe item to select on click
indexnumberOptional index override for highlight matching

Unstable_TriggerPopoverBack

A button that navigates back from items to the category list. Only renders when a category is active.

unstable_useTriggerPopoverScopeContext

Hook to access the popover state and actions for the nearest enclosing Unstable_TriggerPopover.

const {
  open,           // boolean — whether popover is visible
  query,          // string — text after the trigger character
  isSearchMode,   // boolean — whether showing search results
  highlightedIndex,
  categories,
  items,
  activeCategoryId,
  selectCategory,
  selectItem,
  goBack,
  close,
  handleKeyDown,
} = unstable_useTriggerPopoverScopeContext();

unstable_useTriggerPopoverTriggers

Hook to iterate every trigger registered under the current Unstable_TriggerPopoverRoot. Intended for input-level integrations (e.g. Lexical DirectivePlugin). Returns ReadonlyMap<string, Unstable_RegisteredTrigger>.

unstable_useMentionAdapter

Returns a spreadable { adapter, directive } bundle for @ mentions. Reads tools registered via useAssistantTool, accepts explicit items, and supports multi-category drill-down.

import {
  unstable_useMentionAdapter,
  type Unstable_Mention,
} from "@assistant-ui/react";

function MyComposer() {
  // Default: tools from model context as a single "Tools" category
  const mention = unstable_useMentionAdapter();

  return (
    <ComposerTriggerPopover char="@" {...mention} />
  );
}
// Multi-category + tools appended as their own category
const mention = unstable_useMentionAdapter({
  categories: [
    { id: "users", label: "Users", items: [/* ... */] },
    { id: "files", label: "Files", items: [/* ... */] },
  ],
  includeModelContextTools: true,
});

Options:

OptionTypeDescription
itemsUnstable_Mention[]Flat list; ignored when categories is set.
categories{ id, label, items: Unstable_Mention[] }[]Drill-down groups.
includeModelContextToolsboolean | { category?, formatLabel?, icon? }Tools integration. Defaults to true iff neither items nor categories.
formatterUnstable_DirectiveFormatterOverride directive serialization.
onInserted(item) => voidFires after the directive is inserted.
iconMapRecord<string, Unstable_IconComponent>metadata.icon / category id → React component.
fallbackIconUnstable_IconComponentFallback when no iconMap entry matches.

Returns { adapter, directive, iconMap?, fallbackIcon? }.

unstable_useSlashCommandAdapter

Returns a spreadable { adapter, action } bundle for slash commands. Commands are declared as data + inline execute callbacks; execute lives in the hook closure and is never attached to the returned Unstable_TriggerItem, keeping items JSON-serializable.

import {
  unstable_useSlashCommandAdapter,
  type Unstable_SlashCommand,
} from "@assistant-ui/react";

const SLASH_COMMANDS: readonly Unstable_SlashCommand[] = [
  { id: "summarize", execute: () => runSummarize(), icon: "FileText" },
  { id: "translate", execute: () => runTranslate(), icon: "Languages" },
];

function MyComposer() {
  const slash = unstable_useSlashCommandAdapter({ commands: SLASH_COMMANDS });
  return (
    <ComposerTriggerPopover char="/" {...slash} />
  );
}

Pass removeOnExecute: true on the hook options to strip the trigger text from the composer after executing. The hook also accepts iconMap / fallbackIcon options that flow into the returned bundle — {...slash} spreads them into ComposerTriggerPopover.