Reasoning
The Reasoning component displays AI reasoning or thinking content in a collapsible format. It automatically tracks streaming duration and can auto-collapse when streaming completes. It’s a compound component with Reasoning.Root, Reasoning.Trigger, and Reasoning.Content.
Variations
Streaming state
When isStreaming is true, the trigger displays an animated shimmer effect. The component tracks the duration automatically.
With duration
After streaming completes, the component displays how long the thinking took.
Custom thinking message
Customize the thinking message using the getThinkingMessage prop on the trigger.
Installation
import { Reasoning } from '@harnessio/ui/components'
// Basic usage<Reasoning.Root> <Reasoning.Trigger /> <Reasoning.Content> Your reasoning content here... </Reasoning.Content></Reasoning.Root>
// With streaming stateconst [isStreaming, setIsStreaming] = useState(true)
<Reasoning.Root isStreaming={isStreaming}> <Reasoning.Trigger /> <Reasoning.Content> {streamingContent} </Reasoning.Content></Reasoning.Root>
// Controlled open stateconst [open, setOpen] = useState(false)
<Reasoning.Root open={open} onOpenChange={setOpen}> <Reasoning.Trigger /> <Reasoning.Content> Controlled content visibility </Reasoning.Content></Reasoning.Root>Anatomy
Reasoning is a compound component built on Radix UI’s collapsible. Reasoning.Root manages the streaming and open state; the trigger shows the thinking header, and the content holds the reasoning body.
<Reasoning.Root isStreaming={isStreaming}> <Reasoning.Trigger /> <Reasoning.Content>{reasoningText}</Reasoning.Content></Reasoning.Root>Reasoning.Root— the container that manages state and provides context. SetisStreamingwhile the model is thinking; the component tracks duration and can auto-collapse once it finishes. Open state is uncontrolled (defaultOpen, defaulttrue) or controlled (open+onOpenChange).Reasoning.Trigger— the clickable header. By default it renders a chevron followed by a thinking message; while streaming, that message is a shimmer. UsegetThinkingMessageto customize it, or passchildrento replace it entirely.Reasoning.Content— the collapsible body. It renders its children as-is inside a scrollable region and auto-scrolls to the bottom while streaming.
Best practices
Do
- Keep
isStreamingin sync with the model’s state so duration tracking and auto-collapse work as intended. - Let the reasoning collapse after it completes — it’s supporting detail, not the primary answer.
- Use
getThinkingMessagewhen you want a domain-specific label (“Reviewing your code…”) instead of the generic “Thinking…”.
Don’t
- Don’t put the final answer inside
Reasoning.Content; reserve it for the intermediate thinking. - Don’t rely on Markdown formatting in the content — it renders as plain content, not parsed Markdown.
Content
Reasoning.Contentrenders whatever node you pass without Markdown parsing. If you need formatted output, render your own formatted nodes (for example, a Markdown viewer) inside it.- Keep the trigger message short and in the present or past tense that matches the state (“Thinking…” while streaming, “Thought for 5 seconds” after).
Accessibility
Keyboard
- The trigger is a real
<button>(via Radix collapsible): it’s focusable and toggles the content withEnter/Space.
Labeling
- Radix wires the trigger’s
aria-expandedandaria-controlsto the content region, so the expanded/collapsed state is announced.
Behavior
- Auto-duration tracking: when
isStreamingtransitions fromtruetofalse, the component calculates and displays the elapsed time (unless you pass an explicitduration). - Auto-collapse: the component collapses on each transition from streaming to complete, so a finished reasoning block folds itself away.
- Shimmer animation: the default trigger message shows a shimmer while streaming to indicate ongoing activity.
Properties
Reasoning.Root
Prop | Required | Default | Type |
|---|---|---|---|
| isStreaming | false | false | boolean |
| open | false | boolean | |
| defaultOpen | false | true | boolean |
| onOpenChange | false | (open: boolean) => void | |
| duration | false | number | |
| className | false | string |
Reasoning.Trigger
Prop | Required | Default | Type |
|---|---|---|---|
| getThinkingMessage | false | (isStreaming: boolean, duration?: number) => ReactNode | |
| children | false | ReactNode | |
| className | false | string |
Reasoning.Content
Prop | Required | Default | Type |
|---|---|---|---|
| children | true | ReactNode | |
| className | false | string |
useReasoning
Access the reasoning context from within child components. Throws if used outside a Reasoning.Root.
import { useReasoning } from '@harnessio/ui/components'
const CustomComponent = () => { const { isStreaming, isOpen, duration } = useReasoning()
return <div>Streaming: {isStreaming ? 'Yes' : 'No'}</div>}Returns { isStreaming: boolean; isOpen: boolean; duration: number | undefined }.