# Reasoning UI with the Vercel AI SDK

> Forward reasoning with sendReasoning, read reasoning parts in useChat, and show a thinking line with a live timer that folds to 'Thought for 12s' when the answer starts.

Published 2026-11-04. Facts checked 2026-11-04. Source: https://pixilui.com/guides/reasoning-ui-vercel-ai-sdk

With the AI SDK, forward the model's reasoning to the client with `sendReasoning: true` on `toUIMessageStream`. It arrives in `message.parts` as `reasoning` parts with `text` and a `state` of `streaming` or `done`. Show a thinking line with a live timer while the reasoning streams, pass its paragraphs as short steps, and mark it done when the answer's first text part arrives, so it folds to "Thought for 12s".

## How the AI SDK sends reasoning

The [Chatbot](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot) page's Reasoning section says some models send reasoning tokens before the answer and that you forward them with `sendReasoning`. On the client they are parts of type `reasoning` with a `text` property; the docs render them in a `<pre>`. The [Reasoning](https://ai-sdk.dev/docs/ai-sdk-core/reasoning) page covers the provider side: which models return reasoning and how to ask for it.

Two things to plan for in the UI:

- **No reasoning at all.** Many models and settings return none. The thinking line should still show from the moment you send, so the wait has a label.
- **Raw text is long.** Reasoning can run to pages. A line that says what the model is doing, with the detail one click away, reads better than a wall of grey text.

## From parts to the thinking line

| Moment | AI SDK signal | Thinking state props |
| --- | --- | --- |
| Request sent, nothing back | `status === "submitted"` | `label`, `startedAt` |
| Reasoning streaming | A `reasoning` part with `state: "streaming"` | `steps` from its paragraphs, the last one `active` |
| Reasoning done, answer starting | A `text` part appears | `done`; the timer freezes and the line folds |
| Thread reloaded later | Parts from history | `done` with `elapsedMs` you stored |
| Model returns no reasoning | No `reasoning` part | `label` and timer only, `done` when text arrives |

## The code

The route forwards reasoning. The model and its reasoning options come from the provider docs.

```ts
// app/api/chat/route.ts
import { convertToModelMessages, createUIMessageStreamResponse, streamText, toUIMessageStream, type UIMessage } from "ai";

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();
  const result = streamText({ model: "openai/gpt-5.5", messages: await convertToModelMessages(messages) });
  return createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream, sendReasoning: true }) });
}
```

The client turns the reasoning text into steps and folds the line when the answer begins.

```tsx
// components/assistant-turn.tsx
"use client";

import type { UIMessage } from "ai";
import { ThinkingState, type ThinkingStateStep } from "@/components/pixilui/thinking-state";

// One step per paragraph, its first sentence only. The full text stays one click away in your own panel if you want it.
function stepsFrom(reasoning: string, streaming: boolean): ThinkingStateStep[] {
  const paragraphs = reasoning.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean);
  return paragraphs.map((p, i) => ({
    id: String(i),
    text: (p.split(/(?<=[.!?])\s/)[0] ?? p).replace(/\*\*/g, "").slice(0, 90),
    status: streaming && i === paragraphs.length - 1 ? "active" : "done",
  }));
}

export function AssistantTurn({ message, startedAt }: { message: UIMessage; startedAt: number }) {
  const reasoning = message.parts.filter((part) => part.type === "reasoning");
  const answering = message.parts.some((part) => part.type === "text" && part.text.length > 0);
  const streaming = reasoning.some((part) => part.state === "streaming");
  const text = reasoning.map((part) => part.text).join("\n\n");

  return (
    <ThinkingState
      label="Working through your question"
      steps={stepsFrom(text, streaming && !answering)}
      startedAt={startedAt}
      done={answering}
      doneLabel={(ms) => (ms === null ? "Thought it through" : `Thought for ${Math.max(1, Math.round(ms / 1000))}s`)}
    />
  );
}
```

Render `AssistantTurn` above the answer's text parts, with `startedAt` set when you called `sendMessage`. Install it with `npx shadcn@latest add @pixilui/thinking-state`.

## What the thinking line adds

- **A label from the first moment.** The line shows before any reasoning arrives, so a model that returns none still gets a labeled wait.
- **A live timer that freezes.** `startedAt` counts; `done` stops it and the line folds to "Thought for 12s".
- **Steps instead of a transcript.** Short lines, the newest active, the rest checked.
- **Reduced motion.** No moving light, no rise: the steps and the label change at once.

## When the framework already ships a UI

The AI SDK renders reasoning as a `<pre>` in its docs and ships no component. AI Elements, Vercel's component set for the AI SDK, has `Reasoning` and `Chain of Thought`; its site says it moves into shadcn/ui at the end of November 2026. Keep `Reasoning` if your product shows the full reasoning text; the [AI Elements migration doc](/docs/migrate-from-ai-elements) maps it to the thinking line when you want a label, steps and a timer instead.

## When not to use this

- **The reasoning is the product.** A tutor or an audit tool that must show every line of the model's thinking needs a full text panel.
- **Instant answers.** A reply in under a second should not flash a thinking line; show it only after a short delay.
- **Agent runs with tools.** When the model calls tools between thoughts, a [run timeline](/guides/agent-run-timeline-vercel-ai-sdk) shows the steps better.

## Questions

### How do I show reasoning tokens with the AI SDK?

Pass `sendReasoning: true` to `toUIMessageStream` on the server. On the client, reasoning arrives as `reasoning` parts in `message.parts`, each with `text` and a `state`.

### How do I show "Thought for 12 seconds"?

Note when you sent the message, and mark the thinking line done when the first text part of the answer arrives. The elapsed time between the two is the thinking time; store it with the message to show it again after a reload.

### Why are there no reasoning parts in my messages?

Either the model does not return reasoning, or the provider needs it switched on, or the route does not forward it. The AI SDK's Reasoning page covers the provider settings, and `sendReasoning` controls forwarding.

## Related

- [How to show an AI model's reasoning or thinking state](/guides/show-ai-reasoning-thinking-state): what a thinking line needs, framework aside.
- [Reasoning UI with the OpenAI Agents SDK](/guides/reasoning-ui-openai-agents-sdk): the same line from reasoning items.
- [Streaming text with the Vercel AI SDK](/guides/streaming-text-vercel-ai-sdk): the answer that follows the line.

## Sources

- [AI SDK UI: Chatbot, Reasoning](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot), read 4 November 2026
- [AI SDK Core: Reasoning](https://ai-sdk.dev/docs/ai-sdk-core/reasoning), read 4 November 2026
- [ai on npm](https://www.npmjs.com/package/ai) (version 7.0.162, its `ReasoningUIPart` type), read 4 November 2026
- [AI Elements](https://elements.ai-sdk.dev), read 4 November 2026
