Text
Display text with an animated cursor while streaming from AI.
A simple component that shows a blinking cursor at the end of text while content is being streamed from an AI model. Supports smooth character-by-character rendering for a typewriter effect.
Click a button to see streaming text…
Installation
"use client";
import * as React from "react";
import { cn } from "@/lib/cn";
interface StreamingTextProps {
/** The text content to display */
children: string;
/** Whether the text is currently streaming */
streaming?: boolean;
/** Cursor character to show while streaming */
cursor?: string;
/** Enable smooth character-by-character rendering */
smooth?: boolean;
/** Characters per tick when smooth is enabled (default: 2) */
speed?: number;
/** Additional class name for the container */
className?: string;
/** Additional class name for the cursor */
cursorClassName?: string;
}
/**
* Hook that smoothly reveals text character by character
*/
function useSmoothText(
text: string,
enabled: boolean,
charsPerTick = 2,
tickMs = 16,
) {
const [displayedLength, setDisplayedLength] = React.useState(0);
const targetLengthRef = React.useRef(0);
const rafRef = React.useRef<number | null>(null);
const lastTickRef = React.useRef(0);
React.useEffect(() => {
targetLengthRef.current = text.length;
if (!enabled) {
setDisplayedLength(text.length);
return;
}
const animate = (timestamp: number) => {
if (timestamp - lastTickRef.current >= tickMs) {
lastTickRef.current = timestamp;
setDisplayedLength((prev) => {
const next = Math.min(prev + charsPerTick, targetLengthRef.current);
return next;
});
}
if (displayedLength < targetLengthRef.current) {
rafRef.current = requestAnimationFrame(animate);
}
};
rafRef.current = requestAnimationFrame(animate);
return () => {
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
};
}, [text, enabled, charsPerTick, tickMs, displayedLength]);
// Reset when text changes significantly (new response)
React.useEffect(() => {
if (text.length < displayedLength) {
setDisplayedLength(0);
}
}, [text, displayedLength]);
return enabled ? text.slice(0, displayedLength) : text;
}
/**
* StreamingText - Display text with an animated cursor while streaming.
*
* A simple component that shows a blinking cursor at the end of text
* while content is being streamed from an AI model.
*
* @example
* ```tsx
* const { object, isLoading } = useObject({ ... });
*
* <StreamingText streaming={isLoading} smooth>
* {object?.text ?? ""}
* </StreamingText>
* ```
*/
export function StreamingText({
children,
streaming = false,
cursor = "▌",
smooth = false,
speed = 2,
className,
cursorClassName,
}: StreamingTextProps) {
const displayedText = useSmoothText(children, smooth, speed);
const isAnimating = smooth && displayedText.length < children.length;
return (
<span className={cn("whitespace-pre-wrap", className)}>
{displayedText}
{(streaming || isAnimating) && (
<span
className={cn("ml-0.5 inline-block animate-pulse", cursorClassName)}
aria-hidden="true"
>
{cursor}
</span>
)}
</span>
);
}Examples
Smooth Typewriter Effect
Enable smooth character-by-character rendering for a typewriter effect:
Custom Speed
Control how many characters appear per animation frame with the speed prop:
Custom Cursor
Use different cursor characters with the cursor prop:
Streaming data allows for real-time updates and progressive rendering of content.
Block Cursor
Next.js is a React framework that enables server-side rendering and static site generation.
Custom Styling
Style the text and cursor independently with className and cursorClassName:
const greeting = 'Hello, World!';
console.log(greeting);
Static Text (No Cursor)
When streaming is false, the cursor is hidden:
This text appears without a cursor because streaming is disabled.
API Reference
StreamingText
| Prop | Type | Default |
|---|---|---|
children | string | — |
streaming | boolean | false |
cursor | string | "▌" |
smooth | boolean | false |
speed | number | 2 |
className | string | — |
cursorClassName | string | — |