Collapsible accordion for grouping reasoning steps and tool calls.
The ChainOfThought primitive groups consecutive reasoning and tool-call parts into a collapsible accordion. Reasoning models emit reasoning tokens and tool calls before producing a final answer. This primitive lets you collapse those intermediate steps behind a "Thinking" toggle.
Grouped Chain of Thought currently plugs into MessagePrimitive.Parts via components.ChainOfThought. If you're wiring grouped CoT, use that API.
It's currently 22C and partly cloudy in Tokyo.
import {
AuiIf,
ChainOfThoughtPrimitive,
MessagePrimitive,
} from "@assistant-ui/react";
function AssistantMessage() {
return (
<MessagePrimitive.Root>
<MessagePrimitive.Parts>
{({ part }) => {
if (part.type === "text") return <MyText />;
return null;
}}
</MessagePrimitive.Parts>
<ChainOfThought />
</MessagePrimitive.Root>
);
}
function ChainOfThought() {
return (
<ChainOfThoughtPrimitive.Root className="my-2 rounded-lg border">
<ChainOfThoughtPrimitive.AccordionTrigger className="flex w-full cursor-pointer items-center gap-2 px-4 py-2.5 font-medium text-sm hover:bg-muted/50">
Thinking
</ChainOfThoughtPrimitive.AccordionTrigger>
<AuiIf condition={(s) => !s.chainOfThought.collapsed}>
<ChainOfThoughtPrimitive.Parts
components={{
Reasoning: ({ text }) => (
<p className="whitespace-pre-wrap px-4 py-2 text-muted-foreground text-sm italic">
{text}
</p>
),
tools: {
Fallback: ({ toolName, status }) => (
<div className="flex items-center gap-2 px-4 py-2 text-sm">
<span className="font-medium">{toolName}</span>
<span className="text-muted-foreground">
{status.type === "running" ? "running..." : "done"}
</span>
</div>
),
},
}}
/>
</AuiIf>
</ChainOfThoughtPrimitive.Root>
);
}Quick Start
Render your normal message parts with MessagePrimitive.Parts, then place a ChainOfThought component alongside them inside the same MessagePrimitive.Root:
import {
ChainOfThoughtPrimitive,
MessagePrimitive,
} from "@assistant-ui/react";
<MessagePrimitive.Root>
<MessagePrimitive.Parts>
{({ part }) => {
if (part.type === "text") return <MyText />;
return null;
}}
</MessagePrimitive.Parts>
<MyChainOfThought />
</MessagePrimitive.Root>
function MyChainOfThought() {
return (
<ChainOfThoughtPrimitive.Root>
<ChainOfThoughtPrimitive.AccordionTrigger>
Thinking
</ChainOfThoughtPrimitive.AccordionTrigger>
<ChainOfThoughtPrimitive.Parts />
</ChainOfThoughtPrimitive.Root>
);
}Root renders a <div>, AccordionTrigger renders a <button> that toggles the collapsed state, and Parts renders the grouped reasoning and tool-call parts.
Runtime setup: primitives require runtime context. Wrap your UI in AssistantRuntimeProvider with a runtime (for example useLocalRuntime(...)). See Pick a Runtime.
Core Concepts
How Grouping Works
ChainOfThoughtPrimitive.Parts reads the current message's grouped reasoning and tool-call context. In practice, render your normal text/image/data parts with MessagePrimitive.Parts, and render ChainOfThoughtPrimitive separately where you want the collapsible reasoning block to appear.
Collapsed State
The accordion starts collapsed by default. AccordionTrigger toggles between collapsed and expanded. Use AuiIf to conditionally render parts based on the collapsed state:
import { AuiIf, ChainOfThoughtPrimitive } from "@assistant-ui/react";
<ChainOfThoughtPrimitive.Root>
<ChainOfThoughtPrimitive.AccordionTrigger>
Thinking
</ChainOfThoughtPrimitive.AccordionTrigger>
<AuiIf condition={(s) => !s.chainOfThought.collapsed}>
<ChainOfThoughtPrimitive.Parts
components={{ Reasoning }}
/>
</AuiIf>
</ChainOfThoughtPrimitive.Root>Chevron Indicators
Use AuiIf to show directional icons that reflect the current state:
import { AuiIf, ChainOfThoughtPrimitive } from "@assistant-ui/react";
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
<ChainOfThoughtPrimitive.AccordionTrigger className="flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm">
<AuiIf condition={(s) => s.chainOfThought.collapsed}>
<ChevronRightIcon className="size-4" />
</AuiIf>
<AuiIf condition={(s) => !s.chainOfThought.collapsed}>
<ChevronDownIcon className="size-4" />
</AuiIf>
Thinking
</ChainOfThoughtPrimitive.AccordionTrigger>Parts Components
ChainOfThoughtPrimitive.Parts accepts a components prop to control how each part type renders:
<ChainOfThoughtPrimitive.Parts
components={{
Reasoning: ({ text }) => (
<p className="whitespace-pre-wrap px-4 py-2 text-muted-foreground text-sm italic">
{text}
</p>
),
tools: {
Fallback: ({ toolName, status }) => (
<div className="px-4 py-2 text-sm">
{status.type === "running" ? `Running ${toolName}...` : `${toolName} completed`}
</div>
),
},
Layout: ({ children }) => (
<div className="border-t">{children}</div>
),
}}
/>| Prop | Type | Description |
|---|---|---|
components.Reasoning | FC<{ text: string }> | Renders reasoning parts |
components.tools.Fallback | ToolCallMessagePartComponent | Fallback for tool-call parts |
components.Layout | ComponentType<PropsWithChildren> | Wrapper around each rendered part |
Parts
Root
Container for the chain-of-thought disclosure UI. Renders a <div> element unless asChild is set.
<ChainOfThoughtPrimitive.Root className="rounded-lg border">
...
</ChainOfThoughtPrimitive.Root>AccordionTrigger
Trigger that toggles the collapsed state. Renders a <button> element unless asChild is set.
<ChainOfThoughtPrimitive.AccordionTrigger className="flex w-full items-center justify-between px-4 py-2 text-sm">
Thinking
</ChainOfThoughtPrimitive.AccordionTrigger>Parts
Renders reasoning and tool-call parts. This component does not track collapsed state internally, so control visibility with AuiIf as shown in the patterns below.
<ChainOfThoughtPrimitive.Parts
components={{
Reasoning: ({ text }) => (
<p className="whitespace-pre-wrap px-4 py-2 text-muted-foreground text-sm italic">
{text}
</p>
),
tools: {
Fallback: ({ toolName, status }) => (
<div className="px-4 py-2 text-sm">
{status.type === "running" ? `Running ${toolName}...` : `${toolName} completed`}
</div>
),
},
}}
/>Prop
Type
Patterns
Minimal Accordion
function ChainOfThought() {
return (
<ChainOfThoughtPrimitive.Root className="my-2 rounded-lg border">
<ChainOfThoughtPrimitive.AccordionTrigger className="flex w-full cursor-pointer items-center gap-2 px-4 py-2 font-medium text-sm hover:bg-muted/50">
Thinking
</ChainOfThoughtPrimitive.AccordionTrigger>
<AuiIf condition={(s) => !s.chainOfThought.collapsed}>
<ChainOfThoughtPrimitive.Parts
components={{
Reasoning: ({ text }) => (
<p className="whitespace-pre-wrap px-4 py-2 text-muted-foreground text-sm italic">
{text}
</p>
),
}}
/>
</AuiIf>
</ChainOfThoughtPrimitive.Root>
);
}With Tool Calls
function ChainOfThought() {
return (
<ChainOfThoughtPrimitive.Root className="my-2 rounded-lg border">
<ChainOfThoughtPrimitive.AccordionTrigger className="flex w-full cursor-pointer items-center gap-2 px-4 py-2 font-medium text-sm hover:bg-muted/50">
Thinking
</ChainOfThoughtPrimitive.AccordionTrigger>
<AuiIf condition={(s) => !s.chainOfThought.collapsed}>
<ChainOfThoughtPrimitive.Parts
components={{
Reasoning: ({ text }) => (
<p className="whitespace-pre-wrap px-4 py-2 text-muted-foreground text-sm italic">
{text}
</p>
),
tools: {
Fallback: ({ toolName, status }) => (
<div className="flex items-center gap-2 px-4 py-2 text-sm">
<span className="font-medium">{toolName}</span>
<span className="text-muted-foreground">
{status.type === "running" ? "running..." : "done"}
</span>
</div>
),
},
Layout: ({ children }) => (
<div className="border-t">{children}</div>
),
}}
/>
</AuiIf>
</ChainOfThoughtPrimitive.Root>
);
}Relationship to Components
The Chain of Thought guide covers end-to-end setup including backend configuration with reasoning models. See the complete with-chain-of-thought example for a full working implementation.
API Reference
For the complete guide including backend configuration, see Chain of Thought. For prop details, see the ChainOfThoughtPrimitive source.
Related: