# Streaming text with the Vercel AI SDK

> Render useChat's text parts with a streaming text component: pass part.text and part.state, and choose between smoothStream on the server and reveal on the client.

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

With the AI SDK's `useChat`, the answer arrives as text parts in `message.parts`, and each part carries `text` (everything so far) and `state` (`streaming` or `done`). Pass both straight to a streaming text component: `text={part.text}` and `streaming={part.state === "streaming"}`. It reveals whole words at reading pace and holds back half words, so you can leave the server's chunking alone.

## How the AI SDK streams text

On the client, the [Chatbot](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot) page renders `message.parts` and checks the hook's `status` (`submitted`, `streaming`, `ready`, `error`) for the stop button and the spinner. In the `ai` 7.0.162 types we checked, a text part's `text` is the whole text so far and its `state` is `streaming` while it grows and `done` when it ends, which is the shape a streaming text component wants. The same page shows a `throttle` option on `useChat` that limits how often React re-renders while chunks arrive.

On the server, [`smoothStream`](https://ai-sdk.dev/docs/reference/ai-sdk-core/smooth-stream) is a transform for `streamText` that buffers the model's output and releases it by word or by line with a delay between chunks. Its docs warn that word chunking does not work well for Chinese, Japanese, Korean and other languages that do not put spaces between words, and suggest an `Intl.Segmenter` for those.

## Where to smooth

| Choice | Where it runs | Good for |
| --- | --- | --- |
| `smoothStream` on `streamText` | Server | Every client gets even chunks, including ones you do not control |
| `throttle` on `useChat` | Client | Fewer React renders on long answers; does not change pacing |
| `StreamingText` on the text part | Client | Even pacing, half words held, no reflow, a caret, reduced motion |
| All three | Both | Fine: the component paces whatever arrives |

## The code

The route is the plain AI SDK route; `smoothStream` is optional.

```ts
// app/api/chat/route.ts
import { convertToModelMessages, createUIMessageStreamResponse, smoothStream, 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),
    experimental_transform: smoothStream({ chunking: "word" }),
  });
  return createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream }) });
}
```

The client maps the newest answer's text parts to the component, and shows a thinking line until the first part arrives.

```tsx
// components/chat-answer.tsx
"use client";

import { useChat } from "@ai-sdk/react";
import { StreamingText } from "@/components/pixilui/streaming-text";
import { ThinkingState } from "@/components/pixilui/thinking-state";

export function ChatAnswer() {
  const { messages, status } = useChat({ throttle: 50 });
  const last = messages.at(-1);
  const waiting = status === "submitted" || (status === "streaming" && last?.role !== "assistant");

  return (
    <div aria-live="polite">
      {messages.map((message) =>
        message.parts.map((part, i) =>
          // Only the newest answer reveals; earlier messages render as they are.
          part.type === "text" && message.role === "assistant" && message.id === last?.id ? (
            <StreamingText key={`${message.id}-${i}`} text={part.text} streaming={part.state === "streaming"} pauseOnHover />
          ) : part.type === "text" ? (
            <p key={`${message.id}-${i}`}>{part.text}</p>
          ) : null,
        ),
      )}
      {waiting ? <ThinkingState label="Reading your question" /> : null}
    </div>
  );
}
```

Install both with `npx shadcn@latest add @pixilui/streaming-text @pixilui/thinking-state`. Pass `startedAt` to the thinking line, from when you called `sendMessage`, for a live timer.

## What the component adds to the part

- **Words, not chunks.** `smoothStream` sends words; the component reveals them at an even pace on screen, whatever the network does to their timing.
- **Half words held.** A chunk that ends mid-word waits for the rest.
- **No reflow.** Words already shown never move.
- **A regenerate that rewinds.** If the text changes in the middle, the component rewinds to the shared prefix instead of starting over.
- **A caret** while `streaming`, dropped once the last word is shown.

## When the framework already ships a UI

The AI SDK has no text component; its docs render `part.text` in a div. AI Elements, Vercel's component set for the AI SDK, has `Response` and `MessageResponse` for markdown answers; its site says it moves into shadcn/ui at the end of November 2026. Keep a markdown renderer for answers with tables, lists or code blocks; `StreamingText` renders plain text with inline code. The [AI Elements migration doc](/docs/migrate-from-ai-elements) says when to swap which.

## When not to use this

- **Markdown-heavy answers.** Lists, tables and fenced code need a markdown renderer.
- **Text you already have.** Messages loaded from history did not just arrive; render them as plain text instead of replaying the reveal.
- **Structured output.** `streamObject` and output schemas fill forms and tables, not paragraphs.

## Questions

### How do I smooth streaming text in the AI SDK?

On the server, add `experimental_transform: smoothStream()` to `streamText` to release text word by word. On the client, render the text part with a component that reveals whole words at a steady pace and holds half words; you can use both.

### How do I know when an AI SDK text part has finished streaming?

Check `part.state`. It is `streaming` while text arrives and `done` when that part is complete. The hook's `status` tells you about the whole request: `submitted`, `streaming`, `ready` or `error`.

### Does smoothStream work for Chinese or Japanese?

The AI SDK docs say word chunking does not work well for languages that do not separate words with spaces, such as Chinese, Japanese, Korean and Vietnamese, and suggest passing an `Intl.Segmenter` as the chunking option.

## Related

- [How to stream LLM text in React without the layout jumping](/guides/stream-llm-text-react-without-layout-jump): why streamed text jumps, framework aside.
- [Stream LLM text in a Next.js App Router app](/guides/streaming-text-nextjs): the same component with a plain Route Handler.
- [Reasoning UI with the Vercel AI SDK](/guides/reasoning-ui-vercel-ai-sdk): the thinking line before the text.

## Sources

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