Browse all components
Thinking / Reasoning UI
A collapsible chain-of-thought panel — show the model's reasoning trace with elapsed time, folded away by default.
Preview
- 1
Parsed the question
Identified the entity and the comparison dimension.
- 2
Formulated search queries
3 queries across pricing and documentation.
- 3
Scored candidate sources
Ranked 12 results by recency and domain authority.
- 4
Synthesized the answer
Combined 4 sources into a single recommendation.
Presets
Props
Also takes onStop.
Agent promptClaude Code · Cursor · any agent
Add the Thinking / Reasoning UI component from the Scrim UI registry to this project and use it in the configuration described below.
Thinking / Reasoning UI — A collapsible chain-of-thought panel — show the model's reasoning trace with elapsed time, folded away by default.
## 1. Install
```bash
npx shadcn@latest add @scrimui/reasoning
```
This writes a single file to `components/ui/reasoning.tsx`. It is plain React + Tailwind with no runtime dependencies — no Radix, no CVA, nothing to add to package.json. If this project does not use the shadcn CLI, copy the source from https://scrimui.dev/components/reasoning to the same path by hand; nothing in the file depends on shadcn.
## 2. Use it
```tsx
import { Reasoning } from "@/components/ui/reasoning";
const STEPS = [
{ title: "Parsed the question", detail: "Identified the entity and the comparison dimension." },
{ title: "Formulated search queries", detail: "3 queries across pricing and documentation." },
{ title: "Scored candidate sources", detail: "Ranked 12 results by recency and domain authority." },
{ title: "Synthesized the answer", detail: "Combined 4 sources into a single recommendation." },
];
<Reasoning
isThinking
elapsed="2.1s"
defaultOpen
steps={STEPS}
onStop={() => {}}
/>
```
## 3. The configuration that matters
This is the component's "Thinking" state — A live timer and spinner signal active reasoning, with a stop control.
These props were set deliberately and should be preserved as written:
- `isThinking` = true — Still thinking
- `elapsed` = "2.1s" — Elapsed
Every other prop is at its default; leave those off the call site rather than writing the default value out.
## 4. Wire the handlers
`onStop` is a stub in the snippet above (`() => {}`). Connect it to this project's own state and data layer instead of shipping the empty function.
Reference: https://scrimui.dev/components/reasoningFollows the props below — change a control and the prompt changes with it, so an agent reproduces that configuration instead of the defaults.
Install
npx shadcn@latest add @scrimui/reasoningComponent source
React + Tailwind component using shadcn/ui (badge, button, card). The install command adds these dependencies automatically.
When copying source manually, install the required primitives first:
npx shadcn@latest add badge button card"use client";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import * as React from "react";
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
export type ReasoningStep = {
title: string;
detail?: string;
};
export type ReasoningProps = {
steps?: ReasoningStep[];
isThinking?: boolean;
elapsed?: string;
onStop?: () => void;
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
className?: string;
};
/* ------------------------------------------------------------------ */
/* Icons */
/* ------------------------------------------------------------------ */
function BrainIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="15" height="15" {...props}>
<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44A2.5 2.5 0 0 1 4 17.5v-11A2.5 2.5 0 0 1 6.5 4h3Z" />
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44A2.5 2.5 0 0 0 20 17.5v-11A2.5 2.5 0 0 0 17.5 4h-3Z" />
<path d="M12 5v1M12 18v1" />
</svg>
);
}
function ChevronIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="14" height="14" {...props}>
<path d="m6 9 6 6 6-6" />
</svg>
);
}
function StopIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" width="10" height="10" {...props}>
<rect x="6" y="6" width="12" height="12" rx="2" />
</svg>
);
}
function CheckIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="11" height="11" {...props}>
<path d="M20 6 9 17l-5-5" />
</svg>
);
}
/* ------------------------------------------------------------------ */
/* Reasoning */
/* ------------------------------------------------------------------ */
export function Reasoning({
steps = [],
isThinking = false,
elapsed,
onStop,
defaultOpen = true,
open: controlledOpen,
onOpenChange,
className = "",
}: ReasoningProps) {
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
const open = controlledOpen ?? internalOpen;
const setOpen = (v: boolean) => {
setInternalOpen(v);
onOpenChange?.(v);
};
return (
<Card
className={`gap-0 py-0 overflow-hidden rounded-xl border border-border bg-card transition-colors ${className}`}
>
{/* Header */}
<Button variant="ghost" size="sm"
type="button"
onClick={() => setOpen(!open)}
aria-expanded={open}
className="h-auto min-h-8 whitespace-normal justify-start flex w-full items-center gap-2.5 px-3.5 py-2.5 text-left"
>
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
<BrainIcon />
</span>
<span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
{isThinking ? "Reasoning" : "Reasoning trace"}
</span>
{elapsed && <span className="shrink-0 text-xs tabular-nums text-muted-foreground">{elapsed}</span>}
{isThinking && (
<Badge variant="secondary" className="inline-flex shrink-0 items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs text-foreground">
<span className="h-2.5 w-2.5 animate-spin rounded-full border-[1.5px] border-current border-t-transparent" />
Thinking
</Badge>
)}
<span className={`shrink-0 text-muted-foreground transition-transform ${open ? "rotate-180" : ""}`}>
<ChevronIcon />
</span>
</Button>
{/* Steps */}
{open && (
<div className="border-t border-border px-4 py-3">
{steps.length === 0 ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="h-2 w-2 animate-pulse rounded-full bg-muted" />
<span>Formulating an approach…</span>
</div>
) : (
<ol className="relative space-y-3 before:absolute before:left-[7px] before:top-2 before:bottom-2 before:w-px before:bg-muted">
{steps.map((step, i) => (
<li key={i} className="relative pl-6">
<span className="absolute left-0 top-1 flex h-[15px] w-[15px] items-center justify-center rounded-full border border-border bg-card text-xs text-muted-foreground">
{i + 1}
</span>
<p className="text-sm font-medium text-foreground">{step.title}</p>
{step.detail && (
<p className="mt-0.5 text-sm leading-5 text-muted-foreground">
{step.detail}
</p>
)}
</li>
))}
</ol>
)}
{isThinking && onStop && (
<Button variant="outline" size="sm"
type="button"
onClick={onStop}
className="mt-3 inline-flex h-8 items-center gap-1.5 rounded-md border border-border px-2.5 text-xs text-muted-foreground transition-colors hover:bg-muted"
>
<StopIcon />
Stop reasoning
</Button>
)}
{!isThinking && steps.length > 0 && (
<p className="mt-3 flex items-center gap-1.5 text-xs text-emerald-700 dark:text-emerald-400">
<CheckIcon />
Reasoning complete — {steps.length} step{steps.length > 1 ? "s" : ""}
</p>
)}
</div>
)}
</Card>
);
}
When to use it
- Show the trace as it happens — a blank pause during 'thinking' reads as a hang or a failure.
- Keep each step one line when possible; expand details on demand.
- Elapsed time gives users confidence something is progressing. Always show it for long traces.
- Collapse by default once complete so the answer leads the conversation.
- Allow interruption — reasoning can take tens of seconds.
What breaks in production
- Hiding the reasoning entirely. Users of reasoning models expect to see the thinking.
- Letting steps grow unbounded with huge wall-of-text details.
- Auto-scrolling the page as steps stream in — anchor only when the user is already there.
- Using a flashy animation that distracts from the actual content of the trace.
Guides
Related Components
The loading state before the first token — bouncing dots, a blinking caret, or a labeled status line while the model thinks.
A multi-step reasoning trace — each step with its own status and timer, so a long think reads as progress, not a hang.
The message input at the heart of an AI chat app — file attachments, model picker, tool toggles, voice, and a send button that turns into stop.
A chat input that accepts file uploads — attachment chips with upload progress, type icons, and one-click removal.