Back to skills
extension
Category: Development & EngineeringNo API key required

primitives

Guide for assistant-ui UI primitives - ThreadPrimitive, ComposerPrimitive, MessagePrimitive. Use when customizing chat UI components.

personAuthor: jakexiaohubgithub

assistant-ui Primitives

Always consult assistant-ui.com/llms.txt for the latest API.

Composable, unstyled components following Radix UI patterns.

References

Import

import {
  AuiIf,
  ThreadPrimitive,
  ComposerPrimitive,
  MessagePrimitive,
  ActionBarPrimitive,
  BranchPickerPrimitive,
  AttachmentPrimitive,
  ThreadListPrimitive,
  ThreadListItemPrimitive,
} from "@assistant-ui/react";

Primitive Parts

| Primitive | Key Parts | |-----------|-----------| | ThreadPrimitive | .Root, .Viewport, .ViewportFooter, .Messages, .Empty, .ScrollToBottom, .Suggestions, .Suggestion | | ComposerPrimitive | .Root, .Input, .Send, .Cancel, .Attachments, .AddAttachment, .AttachmentDropzone, .Quote, .QuoteText, .QuoteDismiss, .Dictate, .StopDictation, .DictationTranscript, .Queue | | MessagePrimitive | .Root, .Parts (.Content is a deprecated alias), .GroupedParts, .Attachments, .Quote, .GenerativeUI, .Error | | ActionBarPrimitive | .Root, .Copy, .Edit, .Reload, .Speak, .StopSpeaking, .FeedbackPositive, .FeedbackNegative, .ExportMarkdown | | BranchPickerPrimitive | .Root, .Previous, .Next, .Number, .Count | | ThreadListPrimitive | .Root, .New, .Items, .LoadMore | | ThreadListItemPrimitive | .Root, .Trigger, .Title, .Archive, .Unarchive, .Delete | | AttachmentPrimitive | .Root, .Name, .Remove, .unstable_Thumb |

Also exported, each covered by its own reference or skill: ChainOfThoughtPrimitive, SelectionToolbarPrimitive, SuggestionPrimitive, QueueItemPrimitive, ErrorPrimitive, MessagePartPrimitive, AssistantModalPrimitive, ActionBarMorePrimitive, ThreadListItemMorePrimitive.

Custom Thread Example

function CustomThread() {
  return (
    <ThreadPrimitive.Root className="flex flex-col h-full">
      <ThreadPrimitive.Empty>
        <div className="flex-1 flex items-center justify-center">
          Start a conversation
        </div>
      </ThreadPrimitive.Empty>

      <ThreadPrimitive.Viewport className="flex-1 overflow-y-auto p-4">
        <ThreadPrimitive.Messages>
          {({ message }) =>
            message.role === "user" ? (
              <CustomUserMessage />
            ) : (
              <CustomAssistantMessage />
            )
          }
        </ThreadPrimitive.Messages>
      </ThreadPrimitive.Viewport>

      <ComposerPrimitive.Root className="border-t p-4 flex gap-2">
        <ComposerPrimitive.Input className="flex-1 rounded-lg border px-4 py-2" />
        <ComposerPrimitive.Send className="bg-blue-500 text-white px-4 py-2 rounded-lg">
          Send
        </ComposerPrimitive.Send>
      </ComposerPrimitive.Root>
    </ThreadPrimitive.Root>
  );
}

Conditional Rendering

Prefer AuiIf for new code. Primitive .If components still exist but are deprecated.

<AuiIf condition={({ message }) => message.role === "user"}>
  User only
</AuiIf>
<AuiIf condition={({ thread }) => thread.isRunning}>
  Generating...
</AuiIf>
<AuiIf condition={({ message }) => message.branchCount > 1}>
  Has edit history
</AuiIf>

<AuiIf condition={({ thread }) => thread.isRunning}>
  <ComposerPrimitive.Cancel>Stop</ComposerPrimitive.Cancel>
</AuiIf>

<AuiIf condition={({ thread }) => thread.isEmpty}>No messages</AuiIf>

Message Parts (children render function)

As of 0.14, primitives that render lists (ThreadPrimitive.Messages, MessagePrimitive.Parts, ThreadPrimitive.Suggestions, ThreadListPrimitive.Items, ComposerPrimitive.Attachments) take a children render function instead of a components prop. The components prop still works but is deprecated.

MessagePrimitive.Parts is the canonical name (MessagePrimitive.Content is a deprecated alias).

<MessagePrimitive.Parts>
  {({ part }) => {
    switch (part.type) {
      case "text":
        return <p>{part.text}</p>;
      case "image":
        return <img src={part.image} alt="" />;
      case "reasoning":
        return (
          <details>
            <summary>Thinking</summary>
            {part.text}
          </details>
        );
      case "tool-call":
        return part.toolUI ?? <div>Tool: {part.toolName}</div>;
      default:
        return null; // registered tool/data UIs still render
    }
  }}
</MessagePrimitive.Parts>

Returning null from the render function lets registered tool and data UIs render via the registry; return <></> to explicitly render nothing.

Branch Picker

<AuiIf condition={({ message }) => message.branchCount > 1}>
  <BranchPickerPrimitive.Root className="flex items-center gap-1">
    <BranchPickerPrimitive.Previous></BranchPickerPrimitive.Previous>
    <span><BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count /></span>
    <BranchPickerPrimitive.Next></BranchPickerPrimitive.Next>
  </BranchPickerPrimitive.Root>
</AuiIf>

Common Gotchas

Primitives not rendering

  • Wrap in AssistantRuntimeProvider
  • Ensure parent primitive provides context

Styles not applying

  • Primitives are unstyled by default
  • Add className and style with your app's Tailwind/CSS system