03 / 37 · Reasoning
Reasoning panel
A collapsible trace that streams reasoning steps along a timeline, then settles into a summary.
Installation
npx shadcn@latest add "@assistant-ui/elements-reasoning-panel"
Usage
import { ReasoningPanel } from "@/components/elements/reasoning-panel";
<ReasoningPanel
steps={[{ title: "Scope", body: "Find the failing path." }]}
visibleSteps={1}
streaming
open
onOpenChange={setOpen}
restingLabel="Reasoned for 4s"
elapsed="2s"
/>Props
steps*ReasoningStep[]Full list of reasoning steps available to reveal.
visibleSteps*numberHow many steps from the start are currently shown on the timeline.
streaming*booleanWhen true the trigger shows a shimmering Thinking label and the active step pulses.
open*booleanWhether the collapsible step list is expanded.
onOpenChange*(open: boolean) => voidCalled when the user expands or collapses the panel.
restingLabel*stringTrigger label once streaming finishes, for example a summary of how long it thought.
elapsedstringElapsed timer shown next to Thinking while streaming. Omit to hide it.
classNamestringExtra classes merged onto the root.
Source
reasoning-panel.tsx"use client";
import { ChevronDownIcon } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { collapsePanel, mono, SwapLabel } from "./surfaces";
export interface ReasoningStep {
title: string;
body: string;
}
export interface ReasoningPanelProps {
steps: ReasoningStep[];
visibleSteps: number;
streaming: boolean;
open: boolean;
onOpenChange: (open: boolean) => void;
restingLabel: string;
elapsed?: string;
className?: string;
}
export function ReasoningPanel({
steps,
visibleSteps,
streaming,
open,
onOpenChange,
restingLabel,
elapsed,
className,
}: ReasoningPanelProps) {
return (
<Collapsible
open={open}
onOpenChange={onOpenChange}
className={cn("w-full max-w-sm", className)}
>
<CollapsibleTrigger className="group/trigger text-foreground/55 hover:text-foreground/90 flex items-center gap-1.5 py-1 text-[13.5px] transition-[color,scale] outline-none active:scale-[0.98]">
<SwapLabel active={streaming ? 0 : 1} className="text-start">
<>
<span className="relative inline-block leading-none">
<span>Thinking</span>
<span
aria-hidden
className="shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none"
>
Thinking
</span>
</span>
{elapsed !== undefined && (
<span className={cn(mono, "text-foreground/30 tabular-nums")}>
{elapsed}
</span>
)}
</>
<>{restingLabel}</>
</SwapLabel>
<ChevronDownIcon className="size-3.5 shrink-0 opacity-60 transition-transform duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-open/trigger:rotate-180 group-data-panel-open/trigger:rotate-180 motion-reduce:transition-none" />
</CollapsibleTrigger>
<CollapsibleContent className={cn(collapsePanel, "outline-none")}>
<ol className="flex flex-col gap-4 pt-3 pb-1">
{steps.slice(0, visibleSteps).map((step, i) => {
const active = streaming && i === visibleSteps - 1;
return (
<li
key={step.title}
className="fade-in slide-in-from-bottom-1 animate-in fill-mode-both flex gap-3 duration-300"
>
<span
aria-hidden
className={cn(
"mt-[7px] size-[5px] shrink-0 rounded-full transition-colors duration-300",
active
? "animate-pulse bg-blue-500 dark:bg-blue-400"
: "bg-foreground/20",
)}
/>
<span className="flex flex-col">
<p className="text-foreground/90 text-[13.5px] font-medium">
{step.title}
</p>
<p className="text-foreground/50 mt-0.5 text-[13px] leading-relaxed">
{step.body}
</p>
</span>
</li>
);
})}
</ol>
</CollapsibleContent>
</Collapsible>
);
}