---
title: Reasoning
description: A collapsible reasoning disclosure that tracks streaming state and elapsed duration.
source: reasoning
---

```tsx title="primitives/reasoning/demos/basic.tsx"
"use client";

import { Reasoning } from "@intentface/chat/reasoning";
import type { ComponentProps } from "react";

// A completed reasoning block. The parts ship no copy and no layout — the
// trigger label and the section structure below are both yours.
const SECTIONS = [
  {
    header: "Planning the approach",
    body: "Check the existing layout, then decide which axis needs centering.",
  },
  {
    header: "Verifying",
    body: "Confirm the element centers both horizontally and vertically.",
  },
];

export const Basic = () => (
  <div className="w-full max-w-xl">
    <Reasoning.Root defaultOpen className="flex flex-col gap-1">
      <Reasoning.Trigger className="group flex w-fit cursor-pointer items-center gap-2 text-sm text-[#686868] transition-colors hover:text-[#1a1a1a] dark:text-[#9b9b9b] dark:hover:text-[#fcfcfc]">
        <BrainIcon />
        Thought for a few seconds
        <ChevronIcon className="transition-transform group-data-closed:rotate-180" />
      </Reasoning.Trigger>
      <Reasoning.Content className="flex flex-col gap-3 pl-6 text-sm">
        {SECTIONS.map((section) => (
          <div key={section.header} className="flex flex-col gap-0.5">
            <span className="font-medium text-[#1a1a1a] dark:text-[#fcfcfc]">{section.header}</span>
            <p className="leading-[1.7] text-[#686868] dark:text-[#9b9b9b]">{section.body}</p>
          </div>
        ))}
      </Reasoning.Content>
    </Reasoning.Root>
  </div>
);

const BrainIcon = (props: ComponentProps<"svg">) => (
  <svg
    width="16"
    height="16"
    viewBox="0 0 16 16"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.3"
    strokeLinejoin="round"
    aria-hidden="true"
    {...props}
  >
    <path d="M8 2.5a2 2 0 0 0-2 2 2 2 0 0 0-1.5 3.3A2 2 0 0 0 6 11.5a2 2 0 0 0 4 0 2 2 0 0 0 1.5-3.7A2 2 0 0 0 10 4.5a2 2 0 0 0-2-2Z" />
    <path d="M8 2.5v11" />
  </svg>
);

const ChevronIcon = (props: ComponentProps<"svg">) => (
  <svg
    width="14"
    height="14"
    viewBox="0 0 16 16"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.5"
    strokeLinecap="round"
    strokeLinejoin="round"
    aria-hidden="true"
    {...props}
  >
    <path d="m4 10 4-4 4 4" />
  </svg>
);
```

## Usage guidelines

- **Chain-of-thought disclosure** — a collapsible block for a model's thinking.
- **Live label** — shimmers "Thinking…" while streaming and settles to "Thought for Ns" when it stops; the duration is tracked for you.
- **Sectioned content** — bold `**Header**` lines split the text into labelled sections, rendered as markdown.
- **State via `useReasoning`** — read streaming, open, and duration from anywhere inside.
- **Get started** — see [Quick start](/quick-start) to add the package.

## Anatomy

```tsx
<Reasoning.Root isStreaming={isStreaming}>
  <Reasoning.Trigger />
  <Reasoning.Content>{reasoningText}</Reasoning.Content>
</Reasoning.Root>
```

Driven from a streaming message, passing the reasoning parts' text:

```tsx
<Reasoning.Root isStreaming={isLast && isStreaming} defaultOpen={false}>
  <Reasoning.Trigger label={headers} />
  <Reasoning.Content>{texts}</Reasoning.Content>
</Reasoning.Root>
```

## useReasoning

Read the disclosure state from anywhere inside `<Reasoning.Root>`:

export const hookMembers = [
  { name: "isStreaming", type: "boolean", description: "Whether reasoning is still streaming in." },
  { name: "isOpen", type: "boolean", description: "Whether the panel is open." },
  { name: "setIsOpen", type: "(open: boolean) => void", description: "Toggle the panel." },
  { name: "duration", type: "number | undefined", description: "Tracked streaming duration in seconds." },
];

<PropsTable rows={hookMembers} />

## Accessibility

Built on a disclosure: `Reasoning.Trigger` is a `<button>` with `aria-expanded`
and `aria-controls` pointing at the panel, so screen readers announce the
collapsed/expanded state and the relationship. While `isStreaming`, the root
carries `aria-busy` so assistive tech knows the content is still updating.

## API reference

Every part accepts `className`, `style`, and `render` (see
[Styling](/handbook/styling)) and emits a bespoke part attribute (`data-<part>`) unless noted.

### Reasoning

The disclosure root. Renders `data-reasoning`.

export const rootProps = [
  { name: "isStreaming", type: "boolean", default: "false", description: "While true the trigger shimmers; the streamed duration is captured when it flips false." },
  { name: "duration", type: "number", description: "Override the tracked streaming duration (seconds)." },
  { name: "defaultOpen", type: "boolean", default: "false", description: "Uncontrolled initial open state." },
  { name: "open", type: "boolean", description: "Controlled open state." },
  { name: "onOpenChange", type: "(open: boolean) => void", description: "Fires on toggle." },
];

<PropsTable rows={rootProps} />

export const rootAttrs = [
  { attribute: "data-reasoning", description: "The disclosure root." },
  { attribute: "data-streaming", description: "Present while isStreaming is true." },
  { attribute: "data-open", description: "Present while open." },
  { attribute: "data-closed", description: "Present while closed." },
];

<AttributesTable rows={rootAttrs} />

### Reasoning.Trigger

The toggle. Renders a `<button>` with `data-reasoning-trigger`. It ships no
copy — supply the label as children, and read `isStreaming` and `duration` from
`useReasoning()` if you want it to change while thinking.

export const triggerProps = [
  { name: "children", type: "ReactNode", description: "The trigger content — a label, a chevron, whatever you need." },
];

<PropsTable rows={triggerProps} />

export const triggerAttrs = [
  { attribute: "data-reasoning-trigger", description: "The toggle button." },
  { attribute: "data-open", description: "Present while open." },
  { attribute: "data-closed", description: "Present while closed." },
];

<AttributesTable rows={triggerAttrs} />

### Reasoning.Content

The collapsible panel. Renders `data-reasoning-content`. Bold `**Header**`
lines split the text into sections rendered as markdown.

export const contentProps = [
  { name: "children", type: "string | string[]", default: "(required)", description: "Reasoning text; bold headers split it into sections." },
  { name: "keepMounted", type: "boolean", default: "false", description: "Keep the panel in the DOM (hidden) when closed." },
];

<PropsTable rows={contentProps} />

export const contentAttrs = [
  { attribute: "data-reasoning-content", description: "The panel." },
  { attribute: "data-open", description: "Present while open." },
  { attribute: "data-closed", description: "Present while closed." },
  { attribute: "data-starting-style", description: "Present on the first open frame (enter transition)." },
  { attribute: "data-ending-style", description: "Present while the exit animation runs." },
  { attribute: "--panel-height", values: "measured px", description: "The content's natural height, published only while the open or close transition runs so a height transition has a number to animate from. Deliberately released once it settles open, which makes `height: var(--panel-height)` fall back to `auto` so the open panel tracks reasoning text as it streams in." },
];

<AttributesTable rows={contentAttrs} />
