Skip to content
EMBOSS
Docs menu

Streaming Text

Token-streaming text whose tail fades as if still being machined, with a blinking caret slab and aria-busy semantics.

@emboss/streaming-text

Installation

pnpm dlx shadcn@latest add @emboss/streaming-text

Usage

example.tsx
import { StreamingText } from "@/components/ui/streaming-text";

export function Example({ partial, done }: { partial: string; done: boolean }) {
  return <StreamingText text={partial} isStreaming={!done} />;
}

API reference

StreamingText

PropTypeDefaultDescription
textstringThe text received so far.
isStreamingbooleanfalseWhile true: the tail fades, the caret blinks, and aria-busy is set.
Data attributeDescription
data-streamingPresent while text is still arriving.

Source

View source — streaming-text.tsx
streaming-text.tsx
"use client";

import { cn } from "@/lib/utils";

/**
 * Text that arrives in chunks. While streaming, the tail of the passage
 * fades as if still being machined and a caret slab blinks at the end; no
 * per-character animation, no layout thrash. The container reports
 * aria-busy so assistive tech can wait for the settled result — pair it
 * with a `role="log"` parent for polite announcements.
 *
 * @example
 * <StreamingText text={partial} isStreaming={!done} />
 */
function StreamingText({
  className,
  text,
  isStreaming = false,
  ...props
}: Omit<React.ComponentProps<"div">, "children"> & {
  /** The text received so far. */
  text: string;
  /** Whether more is still arriving. */
  isStreaming?: boolean;
}) {
  return (
    <div
      data-slot="streaming-text"
      aria-busy={isStreaming || undefined}
      data-streaming={isStreaming ? "" : undefined}
      className={cn(
        "text-sm leading-relaxed whitespace-pre-wrap text-ink",
        isStreaming &&
          "[mask-image:linear-gradient(to_bottom,black_calc(100%-1.25em),oklch(0_0_0/0.35))]",
        className,
      )}
      {...props}
    >
      {text}
      {isStreaming ? (
        <span
          aria-hidden
          className="ms-0.5 inline-block h-[1em] w-[0.45em] translate-y-[0.15em] animate-depth-pulse rounded-2xs bg-well shadow-deboss-1 motion-reduce:animate-none"
        />
      ) : null}
    </div>
  );
}

export { StreamingText };