03 / 122 · 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.
This element normalizes its counts and shares at the boundary, so a value from outside its range reads as the nearest end rather than reaching the DOM: a negative count shows nothing, one past the end shows everything, and a share stays within 0 to 100 percent. A prop that names a selection is the exception, and means nothing is selected.
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, ShimmerLabel, SwapLabel } from "./surfaces";
import { take } from "./range";
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) {
const shown = take(steps, visibleSteps);
return (
<Collapsible
data-slot="reasoning-panel"
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">
<>
<ShimmerLabel
active={streaming}
className="relative inline-block leading-none"
>
Thinking
</ShimmerLabel>
{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">
{shown.map((step, i) => {
const active = streaming && i === shown.length - 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 min-w-0 flex-1 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 break-words">
{step.body}
</p>
</span>
</li>
);
})}
</ol>
</CollapsibleContent>
</Collapsible>
);
}