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 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 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.
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.
"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.
smoothStreamsends 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 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.
streamObjectand 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: why streamed text jumps, framework aside.
- Stream LLM text in a Next.js App Router app: the same component with a plain Route Handler.
- Reasoning UI with the Vercel AI SDK: the thinking line before the text.
Sources#
- AI SDK UI: Chatbot, read 4 November 2026
- AI SDK Core: smoothStream, read 4 November 2026
- ai on npm (version 7.0.162, its
TextUIParttype), read 4 November 2026 - AI Elements, read 4 November 2026