Elements

Elements · AUI connected · AUI

Thread

A complete chat container with messages, composer, auto-scroll, and accessibility built in.

How can I help you today?

fig. 01

Installation

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

Thread is the complete chat surface: message list, composer, auto-scroll, and the welcome, history-loading, and running states, all wired to a runtime. It has no standalone form, since everything it renders comes from live thread state rather than props you would pass in.

Getting started

Thread needs nothing beyond a runtime provider higher up the tree.

Use it in your app

app/page.tsx
import { Thread } from "@/components/assistant-ui/elements/thread.aui";

export default function Chat() {
  return (
    <div className="h-full">
      <Thread />
    </div>
  );
}

Needs an AssistantRuntimeProvider ancestor; Thread itself takes no runtime prop and reads the nearest one.

Override a slot

import {
  Thread,
  type ThreadComponents,
} from "@/components/assistant-ui/elements/thread.aui";

const THREAD_COMPONENTS: ThreadComponents = {
  ToolFallback: MyToolFallback,
  ToolGroup: MyToolGroup,
};

export default function Chat() {
  return <Thread components={THREAD_COMPONENTS} />;
}

Define components once at module scope, or memoize it, so message subtrees do not re-render whenever the parent does. For per-tool UI, prefer registering a renderer by tool name over overriding ToolFallback: put render on the matching toolkit entry, per Tool UI.

Anatomy

<ThreadPrimitive.Root>
  <ThreadPrimitive.Viewport>
    {/* no messages, and not mid history-load: */}
    <Welcome />
    {/* switched to a thread whose own history is still loading: */}
    <ThreadHistorySkeleton />

    <ThreadPrimitive.Messages>
      {({ message }) =>
        message.role === "user" ? <UserMessage /> : <AssistantMessage />
      }
    </ThreadPrimitive.Messages>

    <ThreadPrimitive.ViewportFooter>
      <ThreadPrimitive.ScrollToBottom />
      <ThreadFollowupSuggestions />
      <Composer />
      {/* new chat, nothing typed yet: */}
      <ThreadPrimitive.Suggestions>
        {() => <SuggestionItem />}
      </ThreadPrimitive.Suggestions>
    </ThreadPrimitive.ViewportFooter>
  </ThreadPrimitive.Viewport>
</ThreadPrimitive.Root>

The welcome screen's condition is narrower than "no messages": at startup the whole thread list can still be loading before this particular thread's history has resolved, and showing the history skeleton for that instant would just flash before turning into the welcome screen anyway. Thread treats "no messages, and either not loading or the whole list is still loading" as the new-chat view, and reserves the skeleton for a thread switch whose history alone is still in flight. Each assistant message's action bar hides while that response is running and, once idle, always shows on the last response (hideWhenRunning, autohide="not-last"); the branch picker beside it hides entirely with a single branch, exactly as on Message branches. Editing a user message swaps it for a composer in place rather than opening a dialog.

Examples

Component overrides

Any slot not passed through components keeps its built-in rendering; only the ones you name are replaced.

const THREAD_COMPONENTS: ThreadComponents = {
  Welcome: () => (
    <div className="mb-6 text-center">
      <h1 className="text-2xl font-medium">Ask me anything</h1>
    </div>
  ),
};

Restyle the shell

Thread sets --thread-max-width, --composer-bg, --composer-radius, and --composer-padding as inline styles directly on ThreadPrimitive.Root. Because they are set there rather than inherited, overriding them means editing your copy of thread.aui.tsx, not layering a class from outside:

style={{
  ["--thread-max-width" as string]: "56rem",
  ["--composer-radius" as string]: "0.75rem",
}}

Suggestions on the welcome screen

import { AssistantRuntimeProvider, AuiConfig, Suggestions } from "@assistant-ui/react";
import { useChatRuntime } from "@assistant-ui/ai-sdk";

function App({ children }: { children: React.ReactNode }) {
  const runtime = useChatRuntime();
  const config = AuiConfig({
    suggestions: Suggestions(["What's the weather?", "Tell me a joke"]),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime} config={config}>
      {children}
    </AssistantRuntimeProvider>
  );
}

See the Suggestions guide for the full configuration surface, including per-suggestion titles and descriptions.

API reference

ThreadProps

PropTypeDefaultDescription
componentsThreadComponents | undefinednoneSlot overrides; see below.
autoFocusboolean | undefinedtrueFocuses the composer on mount, run start, thread switch, and scroll-to-bottom. Set false to leave page focus alone.

ThreadComponents

SlotTypeDescription
AssistantMessageComponentTypeReplaces the entire assistant message, action bar and branch picker included.
WelcomeComponentTypeReplaces the welcome screen shown for a new chat.
ToolFallbackToolCallMessagePartComponentRenders a tool call with no registered UI. A tool UI registered by name takes precedence over this slot.
ToolGroupComponentType<PropsWithChildren<{ group: ThreadGroupPart }>>Wraps a run of consecutive tool calls; receives the group's indices and status.
ReasoningGroupComponentType<PropsWithChildren<{ group: ThreadGroupPart }>>Wraps a run of consecutive reasoning parts; receives the same shape.

Primitives

NamespaceUsed for
ThreadPrimitiveRoot, scrollable viewport, message list, footer, scroll-to-bottom, and welcome suggestions.
ComposerPrimitiveInput, attachment dropzone, send, cancel, and dictation start/stop.
MessagePrimitiveMessage root, rendering a message's parts, grouping consecutive parts by type, and the per-message error slot.
ActionBarPrimitive / ActionBarMorePrimitiveCopy, reload, edit, export-as-markdown, and the overflow menu that holds export.
BranchPickerPrimitiveThe n / m stepper next to each message; see Message branches.
AuiIfEvery conditional slot below.

Thread state

SelectorTypeDescription
s.thread.capabilities.dictationbooleanWhether the composer's mic button can render at all.
s.thread.isRunningbooleanSwaps the composer's send action for cancel.
s.composer.isEmptybooleanGates the welcome suggestions: shown only for a new chat with nothing typed.
s.composer.dictationDictationState | undefinedNon-null while dictation is active; swaps the mic button for a stop button.
s.message.role"user" | "assistant" | "system"Picks the message component to render.
s.message.composer.isEditingbooleanSwaps a user message for its edit composer.
s.message.isCopiedbooleanSwaps the copy icon for a check mark after a copy.