Componenteditors0.87 KB gzipZero dependencies
Streaming Text
Character-by-character stream with cursor.
Preview
Live Preview
Streaming 0/57
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add streaming-textOr install the full npm package:
npm install bigbulluiUsage
Open in StackBlitzimport { StreamingText } from "@/components/ui/streaming-text";
<StreamingText />Usage guidance
Use Streaming Text when a ticket stub surface needs Character-by-character stream with cursor. Keep props minimal and prefer composition with sibling stubs.
Avoid Streaming Text for long form flows or dense data grids where a dedicated layout block fits better.
Source code
streaming-text.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface StreamingTextProps extends React.HTMLAttributes<HTMLDivElement> {
text: string;
speedMs?: number;
showCursor?: boolean;
onDone?: () => void;
}
/** Streams text character by character with a blinking block cursor. */
export function StreamingText({
text,
speedMs = 24,
showCursor = true,
onDone,
className,
...props
}: StreamingTextProps) {
const [count, setCount] = React.useState(0);
const doneRef = React.useRef(false);
React.useEffect(() => {
doneRef.current = false;
setCount(0);
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
setCount(text.length);
onDone?.();
doneRef.current = true;
return;
}
const id = window.setInterval(() => {
setCount((c) => {
if (c >= text.length) {
window.clearInterval(id);
if (!doneRef.current) {
doneRef.current = true;
onDone?.();
}
return c;
}
return c + 1;
});
}, Math.max(4, speedMs));
return () => window.clearInterval(id);
}, [text, speedMs, onDone]);
const streaming = count < text.length;
return (
<div
className={cn("w-full rounded-lg border border-dashed border-border bg-card p-3", className)}
aria-live="polite"
{...props}
>
<p className="text-sm leading-6 text-foreground">
{text.slice(0, count)}
{showCursor && (
<span
aria-hidden="true"
className={cn(
"ml-0.5 inline-block h-4 w-2 translate-y-0.5 bg-accent",
streaming ? "animate-pulse motion-reduce:animate-none" : "opacity-40",
)}
/>
)}
</p>
<p className="mt-2 font-mono text-[10px] uppercase tracking-wider text-muted-foreground">
{streaming ? `Streaming ${count}/${text.length}` : "Stream complete"}
</p>
</div>
);
}