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.

Report a bug

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 components

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/thinking-block.json

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

Code

"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">&hellip;</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> ); }

Dependencies

  • @vllnt/ui@^0.2.1
  • lucide-react