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'
2
3<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);
4
5 React.useEffect(() => () => clearTimeout(timerRef.current), []);
6
7 return (
8 <Flex direction="column" gap={4} style={{ width: 420 }}>
9 <Button
10 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);
3
4 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:

SlotElement
reasoningRoot container
reasoning-triggerThe button that expands and collapses the panel
reasoning-chevronChevron inside the trigger
reasoning-label-streamingTrigger label while the model is still reasoning
reasoning-panelThe collapsible panel
reasoning-bodyContent inside the panel
reasoning-stepOne reasoning step
reasoning-step-labelThat step's label
reasoning-step-bodyThat step's content

Accessibility

  • Inherits Collapsible semantics: the trigger exposes aria-expanded and toggles with Enter and Space.
  • The "Thinking…" shimmer and the panel's height tween pause under prefers-reduced-motion: reduce.