Thinking Block
Collapsible React block that streams an AI's thinking/reasoning steps. Surface chain-of-thought without cluttering the final answer. Install via the shadcn CLI.
When to use this in an AI app
Use Thinking Block to reveal an agent's reasoning or scratchpad separately from its final answer. Collapsible plus streaming support keeps the transcript clean while staying transparent.
Browse all AI agent componentsPreview
Switch between light and dark to inspect the embedded Storybook preview.
Installation
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/thinking-block.jsonStorybook
Explore all variants, controls, and accessibility checks in the interactive Storybook playground.
View in StorybookCode
"use client";
import { useCallback, useEffect, useId, useState } from "react";
import { ChevronDown, ChevronRight } from "lucide-react";
import { cn } from "../../lib/utils";
export type ThinkingBlockProps = {
className?: string;
/** Whether the content is still streaming. */
isStreaming?: boolean;
/** The thinking text content to display. */
thinking: string;
};
/** Collapsible thinking block with streaming support. */
export function ThinkingBlock({
className,
isStreaming = false,
thinking,
}: ThinkingBlockProps) {
const [isExpanded, setIsExpanded] = useState(isStreaming);
const contentId = useId();
// Auto-open when streaming starts
useEffect(() => {
if (isStreaming) {
requestAnimationFrame(() => {
setIsExpanded(true);
});
}
}, [isStreaming]);
const toggleExpanded = useCallback(() => {
setIsExpanded((previous) => !previous);
}, []);
return (
<div className={cn("mb-2", className)}>
<button
aria-controls={contentId}
aria-expanded={isExpanded}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
onClick={toggleExpanded}
type="button"
>
{isExpanded ? (
<ChevronDown className="size-3" />
) : (
<ChevronRight className="size-3" />
)}
<span>
Thinking
{isStreaming ? (
<span className="ml-1 animate-pulse">…</span>
) : null}
</span>
</button>
{isExpanded ? (
<div
className="mt-2 p-3 bg-muted/50 rounded text-xs text-muted-foreground whitespace-pre-wrap border-l border-muted-foreground/30 max-h-48 overflow-y-auto"
id={contentId}
>
{thinking}
{isStreaming ? <span className="animate-pulse">|</span> : null}
</div>
) : null}
</div>
);
}