Skip to main content
NachUI

Reasoning

The model's thinking, open while it streams and folded away once the answer starts.

The user wants a copy-paste component, so an npm package is off the table. I will read the registry first, then write the primitive with no runtime dependencies beyond the ones the library already ships.
1'use client';
2
3import { Reasoning } from '@/components/ui/reasoning';
4
5export function Default() {
6 return (
7 <div className="w-full max-w-md">
8 <Reasoning duration={4}>
9 <Reasoning.Trigger />
10 <Reasoning.Content>
11 The user wants a copy-paste component, so an npm package is off the table. I will read the
12 registry first, then write the primitive with no runtime dependencies beyond the ones the
13 library already ships.
14 </Reasoning.Content>
15 </Reasoning>
16 </div>
17 );
18}

Installation

pnpm dlx nachui add reasoning

Anatomy

1import { Reasoning } from '@/components/ui/reasoning';
1<Reasoning isStreaming={isReasoning}>
2 <Reasoning.Trigger />
3 <Reasoning.Content>{reasoningText}</Reasoning.Content>
4</Reasoning>
The trigger writes its own label. While isStreaming is true it pulses "Thinking", and once the stream ends it settles into "Thought for 4 seconds" using either the duration you pass or the time it measured itself.

Composition

Use the following composition to build a Reasoning:
Reasoning
├── Reasoning.Trigger
└── Reasoning.Content

Streaming

Flipping isStreaming on opens the panel, and flipping it off closes it and records the duration. That is the behaviour people expect from a thinking model: you see the reasoning arrive, then it gets out of the way, and it is one click back.
1'use client';
2
3import { useState } from 'react';
4import { Reasoning } from '@/components/ui/reasoning';
5
6export function Streaming() {
7 const [streaming, setStreaming] = useState(false);
8
9 return (
10 <div className="flex w-full max-w-md flex-col gap-4">
11 <button
12 type="button"
13 onClick={() => setStreaming((previous) => !previous)}
14 className="border-border hover:bg-muted w-fit rounded-full border px-3 py-1.5 text-xs transition-colors"
15 >
16 {streaming ? 'Stop streaming' : 'Start streaming'}
17 </button>
18 <Reasoning isStreaming={streaming} defaultOpen={false}>
19 <Reasoning.Trigger />
20 <Reasoning.Content>
21 The panel opens on its own while the model is thinking and closes once the answer starts,
22 keeping the reasoning one click away instead of in the way.
23 </Reasoning.Content>
24 </Reasoning>
25 </div>
26 );
27}

useReasoning

useReasoning() reads the state from inside the panel, which is how you build a custom trigger or a footer that reacts to the stream.
1const { isStreaming, isOpen, setIsOpen, duration } = useReasoning();

API Reference

Reasoning

PropTypeDefaultDescription
isStreamingbooleanfalseOpens the panel, pulses the label, times the run
defaultOpenbooleantrueOpen state when uncontrolled
openboolean-Controlled open state
onOpenChange(open: boolean) => void-Called when the open state changes
durationnumber-Seconds to display instead of the measured time
classNamestring-Additional CSS classes

Reasoning.Trigger

PropTypeDefaultDescription
getLabel(isStreaming: boolean, duration?: number) => ReactNode-Replaces the built-in wording
childrenReactNode-Static label, wins over getLabel
classNamestring-Additional CSS classes

Reasoning.Content

PropTypeDefaultDescription
classNamestring-Additional CSS classes
Found something to improve?

Notice a bug, typo, or missing detail on this page? Help us make the documentation better by opening a GitHub issue.

Create an Issue