Reasoning
Collapsible "thinking" section for AI responses — auto-opens while streaming, auto-collapses on completion, with labelled steps.
1<div style={{ width: 420 }}>2 <Reasoning duration={10} defaultOpen>3 <Reasoning.Trigger />4 <Reasoning.Content>5 <Reasoning.Step label="Gathering project context">6 <Text size="small" variant="secondary">7 Looked at recent issues in Design System 2.8 </Text>9 </Reasoning.Step>10 <Reasoning.Step label="Creating the task" />11 </Reasoning.Content>12 </Reasoning>13</div>
Anatomy
1import { Reasoning } from '@raystack/apsara'23<Reasoning streaming={isThinking} duration={10}>4 <Reasoning.Trigger />5 <Reasoning.Content>6 <Reasoning.Step label="Gathering ticket updates">7 {/* indented detail */}8 </Reasoning.Step>9 </Reasoning.Content>10</Reasoning>
Built on Collapsible. Drive streaming from your transport; everything
here is presentational and works anywhere — inside a
Message body, a Chat scroller, or on its own.
Usage
Left alone, the section opens while the model is thinking and closes once it finishes. These are the ways to change that.
Streaming
Drive streaming from your transport; the trigger label and open state
follow along.
1(function StreamingReasoning() {2 const [streaming, setStreaming] = React.useState(false);3 const timerRef = React.useRef(null);45 React.useEffect(() => () => clearTimeout(timerRef.current), []);67 return (8 <Flex direction="column" gap={4} style={{ width: 420 }}>9 <Button10 size="small"11 variant="outline"12 color="neutral"13 onClick={() => {14 setStreaming(true);15 timerRef.current = setTimeout(() => setStreaming(false), 3000);
Custom trigger label
Replace the default trigger text when "Reasoning" is not the right word for what the model is doing.
1<div style={{ width: 420 }}>2 <Reasoning duration={7}>3 <Reasoning.Trigger>Show the plan</Reasoning.Trigger>4 <Reasoning.Content>5 <Reasoning.Step label="Outline the migration" />6 <Reasoning.Step label="Estimate the blast radius" />7 </Reasoning.Content>8 </Reasoning>9</div>
Controlled
Pass open with onOpenChange to own the panel — needed when reasoning should expand automatically while the model streams and collapse once it finishes.
1(function ControlledReasoning() {2 const [open, setOpen] = React.useState(false);34 return (5 <Flex direction="column" gap={5} style={{ width: "100%" }}>6 <Button size="small" variant="outline" onClick={() => setOpen((o) => !o)}>7 {open ? "Hide reasoning" : "Show reasoning"}8 </Button>9 <Reasoning open={open} onOpenChange={setOpen} duration={4}>10 <Reasoning.Trigger />11 <Reasoning.Content>12 <Reasoning.Step label="Reading the ticket">13 Pulled the last three comments.14 </Reasoning.Step>15 <Reasoning.Step label="Drafting a reply">
API Reference
A root, the trigger that toggles it, and the content — with Step for a single line of thinking.
Reasoning
Prop
Type
The open state auto-follows streaming (open while true, collapsed once it
flips back) until the user toggles the panel manually — from then on it's
theirs.
Reasoning.Trigger
Renders the default status label (shimmering "Thinking…", then "Worked for N seconds") plus a chevron; pass children to replace the label.
Reasoning.Content
The collapsible panel holding the steps.
Reasoning.Step
A labelled row with optional indented detail.
Prop
Type
Slots
Every rendered part carries a stable data-slot attribute for styling and testing:
| Slot | Element |
|---|---|
reasoning | Root container |
reasoning-trigger | The button that expands and collapses the panel |
reasoning-chevron | Chevron inside the trigger |
reasoning-label-streaming | Trigger label while the model is still reasoning |
reasoning-panel | The collapsible panel |
reasoning-body | Content inside the panel |
reasoning-step | One reasoning step |
reasoning-step-label | That step's label |
reasoning-step-body | That step's content |
Accessibility
- Inherits Collapsible semantics: the trigger exposes
aria-expandedand toggles with Enter and Space. - The "Thinking…" shimmer and the panel's height tween pause under
prefers-reduced-motion: reduce.