Browse all components
Reasoning Steps UI
A multi-step reasoning trace — each step with its own status and timer, so a long think reads as progress, not a hang.
Preview
- Parsing your question
- Planning the search
- Querying three sources
- Extracting citations
- Writing the answer
Presets
Props
Agent promptClaude Code · Cursor · any agent
Add the Reasoning Steps UI component from the Scrim UI registry to this project and use it in the configuration described below.
Reasoning Steps UI — A multi-step reasoning trace — each step with its own status and timer, so a long think reads as progress, not a hang.
## 1. Install
```bash
npx shadcn@latest add @scrimui/reasoning-steps
```
This writes a single file to `components/ui/reasoning-steps.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-steps to the same path by hand; nothing in the file depends on shadcn.
## 2. Use it
```tsx
import { ReasoningSteps } from "@/components/ui/reasoning-steps";
const STEPS = [
"Parsing your question",
"Planning the search",
"Querying three sources",
"Extracting citations",
"Writing the answer",
];
<ReasoningSteps
title="Reasoning"
activeStep={2}
elapsed="3.2s"
defaultExpanded
steps={STEPS}
/>
```
## 3. The configuration that matters
This is the component's "Expanded" state — Finished steps show a check, the running one a spinner, later ones stay dim.
Every prop is at its default value. Keep the call site minimal — do not write out default values.
Reference: https://scrimui.dev/components/reasoning-stepsFollows 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/reasoning-stepsComponent source
React + Tailwind component using shadcn/ui (button). The install command adds these dependencies automatically.
When copying source manually, install the required primitives first:
npx shadcn@latest add button"use client";
import { Button } from "@/components/ui/button";
import * as React from "react";
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
export type ReasoningStepsProps = {
/** Each step's short label, in order. */
steps: string[];
/** Index of the step currently running — earlier steps render as done, later ones as pending. */
activeStep?: number;
/** Time spent on the active step, shown next to it (e.g. "3.2s"). */
elapsed?: string;
defaultExpanded?: boolean;
title?: string;
className?: string;
};
/* ------------------------------------------------------------------ */
/* Icons */
/* ------------------------------------------------------------------ */
function ChevronIcon({ open }: { open: boolean }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
width="14"
height="14"
className={`transition-transform ${open ? "rotate-180" : ""}`}
>
<path d="m6 9 6 6 6-6" />
</svg>
);
}
function CheckIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="13" height="13">
<path d="M20 6 9 17l-5-5" />
</svg>
);
}
/* ------------------------------------------------------------------ */
/* ReasoningSteps */
/* ------------------------------------------------------------------ */
export function ReasoningSteps({
steps,
activeStep = -1,
elapsed,
defaultExpanded = true,
title = "Reasoning",
className = "",
}: ReasoningStepsProps) {
const [open, setOpen] = React.useState(defaultExpanded);
const doneCount = steps.filter((_, i) => i < activeStep).length;
return (
<div className={`overflow-hidden rounded-xl border border-border ${className}`}>
<Button variant="ghost" size="sm"
type="button"
onClick={() => setOpen((o) => !o)}
className="h-auto min-h-8 whitespace-normal justify-start flex w-full items-center gap-2 bg-muted px-3 py-2.5 text-left transition-colors hover:bg-muted"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="14" height="14" className="text-muted-foreground">
<path d="M9.18 9.5a3 3 0 0 0 2.42 4.73" />
<path d="M12 2a10 10 0 1 0 10 10" />
<path d="M12 6v4l3 2" />
</svg>
<span className="text-sm font-medium text-foreground">{title}</span>
{activeStep >= 0 && activeStep < steps.length ? (
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<span className="h-3 w-3 animate-spin rounded-full border-2 border-border border-t-foreground " />
Step {activeStep + 1} of {steps.length}
{elapsed && ` · ${elapsed}`}
</span>
) : (
<span className="text-xs text-muted-foreground">
{doneCount} {doneCount === 1 ? "step" : "steps"} · {steps.length} total
</span>
)}
<span className="ml-auto text-muted-foreground">
<ChevronIcon open={open} />
</span>
</Button>
{open && (
<ol className="border-t border-border p-2">
{steps.map((step, i) => {
const state = i < activeStep ? "done" : i === activeStep ? "active" : "pending";
return (
<li
key={step}
className={`flex items-start gap-2.5 rounded-lg px-2 py-1.5 ${
state === "active" ? "bg-muted" : ""
}`}
>
<span className="mt-[3px] flex h-4 w-4 shrink-0 items-center justify-center">
{state === "done" && (
<span className="flex h-4 w-4 items-center justify-center rounded-full bg-emerald-100 text-emerald-600 dark:bg-emerald-900/40 dark:text-emerald-400">
<CheckIcon />
</span>
)}
{state === "active" && (
<span className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-border border-t-foreground " />
)}
{state === "pending" && (
<span className="h-1.5 w-1.5 rounded-full bg-muted" />
)}
</span>
<span
className={`text-sm leading-5 ${
state === "done"
? "text-muted-foreground"
: state === "active"
? "font-medium text-foreground"
: "text-muted-foreground"
}`}
>
{step}
</span>
</li>
);
})}
</ol>
)}
</div>
);
}
When to use it
- Show a step trace when the agent's work is a sequence the user can reason about — search, files, tools — not for a single undivided answer.
- Collapse the trace by default and let the header live-update; the summary is the status, the list is the detail.
- Color the running step subtly; the spinner and elapsed time carry the motion, not a flashing background.
- Keep steps short and verb-first ('Querying sources', not 'Source query process').
What breaks in production
- Revealing every internal step — a trace is a promise of progress, not a license to dump the whole plan.
- Letting the trace jump around as steps resolve; stable ordering beats live re-sorting.
- Animation without an end state; when done, replace the spinner with a check so the trace reads finished.
Guides
A task that runs for minutes needs more than a spinner: live progress, visible steps, and an escape hatch. Here is how the products that ship long-running agents keep users oriented — and in control.
Every long-running agent needs a way out — but a stop button that discards work is as bad as none. Here is how real products handle interruption: non-destructively, cleanly, and with the partial work kept.
Related Components
A collapsible chain-of-thought panel — show the model's reasoning trace with elapsed time, folded away by default.
The loading state before the first token — bouncing dots, a blinking caret, or a labeled status line while the model thinks.
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.