Elements

Streaming text

Tokens arrive softly: the newest words land in blue and settle into ink.

fig. 01 · plays once, replay from the corner

Installation

npx shadcn@latest add "@assistant-ui/elements-streaming-text"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

Word by word text with the newest handful tinted blue and a caret at the end while it streams; older words fade back to plain text over the next few hundred milliseconds. With a runtime you feed it a part's growing text and its running status; standalone you hold the segments and a reveal count yourself.

Getting started

MessagePrimitive.Parts hands a custom part renderer the part's own fields as props, text and status included, so there is no selector to write at all: count just tracks how many words that string currently has.

Register it as a part renderer

components/assistant-ui/elements/streaming-text.tsx
"use client";

import { useMemo } from "react";
import type { TextMessagePartComponent } from "@assistant-ui/react";
import { StreamingText, type Segment } from "@/components/assistant-ui/elements/streaming-text";

const StreamingTextPart: TextMessagePartComponent = ({ text, status }) => {
  const segments = useMemo<Segment[]>(() => [{ text }], [text]);
  const count = useMemo(() => text.split(" ").length, [text]);
  return <StreamingText segments={segments} count={count} streaming={status.type === "running"} />;
};

TextMessagePartComponent is the same component type MessagePrimitive.Parts expects for its Text slot, so no wrapper or adapter is needed between the two.

Plug it into the parts list

import { MessagePrimitive } from "@assistant-ui/react";

<MessagePrimitive.Parts components={{ Text: StreamingTextPart, Reasoning: StreamingTextPart }} />

The same component works for both slots: a ReasoningMessagePartComponent carries the same text and status fields. Once a part's status.type moves to "complete", the caret disappears and the last words settle to plain text on their own.

Anatomy

<p data-slot="streaming-text">
  {/* revealed words, newest two tinted while streaming */}
  <span aria-hidden />{/* caret, only while streaming and at least one word is shown */}
</p>

Every segment's text is split on spaces and rejoined into words separated by a single space each: a segment's own line breaks or repeated spaces do not survive into the rendered output. count clamps between 0 and the total word count (a negative or NaN count shows nothing, an overly large one shows everything). While streaming is true, the last two words currently shown render tinted; every earlier word is plain text, and a word already past that trailing window transitions back to plain over 700ms rather than snapping. The trailing caret only renders when streaming is true and at least one word is shown.

Examples

Marking a span as code

const segments: Segment[] = [
  { text: "Read the value from" },
  { text: "response.data", mono: true },
  { text: "before formatting it." },
];

Restyle the text

Both lanes take className on the root, which starts as min-h-[8.5rem] max-w-sm text-sm leading-relaxed. The tint and the caret are both fixed to text-blue-500/bg-blue-500; there is no prop for either color.

<StreamingText className="max-w-none text-base" /* ... */ />

API reference

Part renderer props

PropTypeDescription
textstringThe part's string streamed so far.
statusMessagePartStatusstatus.type === "running" until the part settles.

These arrive on any component registered under MessagePrimitive.Parts's Text or Reasoning slot; both TextMessagePartComponent and ReasoningMessagePartComponent carry them.