Join our Discord Community

React AI Reasoning

Collapsible AI reasoning display with auto-streaming behavior. Build transparent AI interfaces with React, Next.js, and TypeScript, featuring automatic open/close and duration tracking for shadcn/ui applications.

Trying to implement AI Elements?

Join our Discord community for help from other developers.


Collapsible reasoning display for AI models that show their thinking process in conversational AI applications. This free open source React component automatically opens during reasoning, closes when complete, and tracks thinking duration in Next.js projects.

Auto-collapsing reasoning display

Thinking indicator with expandable content:

Loading component...

Automatically opens during streaming in React applications, closes when complete in TypeScript components, tracks thinking duration, and provides smooth animations. Works with any reasoning-capable AI model in JavaScript frameworks.

Installation

npx shadcn@latest add https://www.shadcn.io/registry/ai.json
npx shadcn@latest add https://www.shadcn.io/registry/ai.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/ai.json
bunx shadcn@latest add https://www.shadcn.io/registry/ai.json

Usage

import {
  Reasoning,
  ReasoningTrigger,
  ReasoningContent,
} from "@/components/ai/reasoning";

<Reasoning isStreaming={isThinking}>
  <ReasoningTrigger title="Thinking" />
  <ReasoningContent>Let me think about this step by step...</ReasoningContent>
</Reasoning>;

Why show AI reasoning?

Users don't trust black box responses for important decisions in React applications. Showing the thinking process builds confidence and lets users verify the logic in Next.js projects.

Duration tracking shows effort in TypeScript components. "Thought for 8 seconds" signals deliberate consideration, not instant pattern matching in JavaScript implementations.

Usage with Vercel AI SDK

Stream reasoning from models that support it using Vercel AI SDK in React applications:

"use client";

import {
  Reasoning,
  ReasoningTrigger,
  ReasoningContent,
} from "@/components/ai/reasoning";
import {
  Conversation,
  ConversationContent,
} from "@/components/ai/conversation";
import { Message, MessageContent } from "@/components/ai/message";
import {
  PromptInput,
  PromptInputTextarea,
  PromptInputSubmit,
} from "@/components/ai/prompt-input";
import { Response } from "@/components/ai/response";
import { useChat } from "@ai-sdk/react";
import { useState } from "react";

export default function ReasoningChat() {
  const [input, setInput] = useState("");
  const { messages, append, status } = useChat();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (input.trim()) {
      append({ role: "user", content: input });
      setInput("");
    }
  };

  return (
    <div className="flex flex-col h-full max-w-4xl mx-auto">
      <Conversation>
        <ConversationContent>
          {messages.map((message) => (
            <Message from={message.role} key={message.id}>
              <MessageContent>
                {message.parts?.map((part, i) => {
                  switch (part.type) {
                    case "text":
                      return (
                        <Response key={`${message.id}-${i}`}>
                          {part.text}
                        </Response>
                      );
                    case "reasoning":
                      return (
                        <Reasoning
                          key={`${message.id}-${i}`}
                          isStreaming={status === "streaming"}
                          defaultOpen={false}
                        >
                          <ReasoningTrigger />
                          <ReasoningContent>{part.text}</ReasoningContent>
                        </Reasoning>
                      );
                    default:
                      return null;
                  }
                })}
              </MessageContent>
            </Message>
          ))}
        </ConversationContent>
      </Conversation>

      <PromptInput onSubmit={handleSubmit}>
        <PromptInputTextarea
          value={input}
          onChange={(e) => setInput(e.currentTarget.value)}
          placeholder="Ask me to reason through something..."
        />
        <PromptInputSubmit disabled={!input.trim()} status={status} />
      </PromptInput>
    </div>
  );
}

Backend route with reasoning support:

// app/api/chat/route.ts
import { streamText, convertToModelMessages } from "ai";

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: "deepseek/deepseek-r1", // or other reasoning-capable model
    messages: convertToModelMessages(messages),
  });

  return result.toUIMessageStreamResponse({
    sendReasoning: true, // Enable reasoning part streaming
  });
}

Features

  • Automatic open/close behavior during reasoning streams (opens when reasoning starts, closes when done) in React applications
  • Manual toggle control with smooth animations in TypeScript components
  • Duration tracking with "Thought for X seconds" display for JavaScript frameworks
  • Controllable state for custom behavior in Next.js projects
  • Accessible keyboard navigation and screen reader support
  • Works with any Vercel AI SDK setup and AI chat applications
  • Free open source component designed for reasoning-capable AI models (DeepSeek R1, o1, etc.) in conversational AI interfaces

API Reference

Reasoning

Container component managing reasoning state and auto-behavior.

PropTypeDefaultDescription
isStreamingbooleanfalseAuto-opens when true, auto-closes when false
openboolean-Controlled open state
defaultOpenbooleanfalseInitial open state for uncontrolled usage
onOpenChange(open: boolean) => void-Callback when open state changes
durationnumber0Controlled duration in seconds
classNamestring-Additional CSS classes
...propsComponentProps<typeof Collapsible>-Collapsible component props

ReasoningTrigger

Clickable trigger showing reasoning status and duration.

PropTypeDefaultDescription
titlestring"Reasoning"Custom title for thinking state
classNamestring-Additional CSS classes
...propsComponentProps<typeof CollapsibleTrigger>-Collapsible trigger props

ReasoningContent

Content container for reasoning text with animations.

PropTypeDescription
childrenstringRequired - Reasoning content text
classNamestringAdditional CSS classes
...propsComponentProps<typeof CollapsibleContent>Collapsible content props

Keyboard interactions

KeyDescription
Space / EnterToggle reasoning visibility on trigger
TabFocus reasoning trigger
EscapeClose reasoning panel when focused

Reasoning display gotchas

Auto-close timing can feel abrupt: The React component auto-closes 1 second after streaming ends. Adjust AUTO_CLOSE_DELAY if users complain in TypeScript implementations.

Long reasoning breaks mobile: DeepSeek R1 reasoning can be 2000+ words in JavaScript applications. Set max-height and scroll, or mobile users scroll forever in Next.js projects.

Streaming performance issues: Large reasoning content updates rapidly in React components. Consider throttling updates for smoother animations.

Default open state matters: Most users want reasoning closed by default in TypeScript applications. They'll expand it if interested.

Empty reasoning handling: Not all models provide reasoning for every response in JavaScript frameworks. Handle undefined reasoning gracefully in React implementations.

Integration with other components

Works great with Response for formatted reasoning content in React applications. Drop into Message for chat-based reasoning display in Next.js projects. Combine with Conversation for scrolling reasoning streams. This free open source component integrates seamlessly with modern JavaScript frameworks.

Questions developers actually ask