Skip to main content
Back to discovery

Live preview recorded from Mellow UI; component and demo published by Mellow UI. Preview: platform recorded · rights authorized.

Streaming Text

An AI-response text renderer — tokens materialize out of a blur at stream pace, trailed by a blinking caret that vanishes on completion.

MSource LandMellow UI

Why it stands out

An AI-response text renderer — tokens materialize out of a blur at stream pace, trailed by a blinking caret that vanishes on completion.

Prompt

You are given a task to integrate a React component into your codebase.
Please verify your project has the following setup:
- shadcn/ui project structure
- Tailwind CSS v4.0
- TypeScript
- motion/react v11 (install: `pnpm add motion`)

If any of these are missing, provide instructions on how to setup project via shadcn CLI, install Tailwind or TypeScript.

Determine the default path for components and styles. Mellow components live in `components/mellow/`. If that folder does not exist yet, create it.

Copy-paste these files:

File location: components/streaming-text-demo.tsx

File content: "use client";

import React, { useState } from "react";
import { StreamingText } from "../mellow/streaming-text";

const ANSWER =
  "Streaming is mostly an illusion of care. The model produces tokens either way — but revealing them at a human reading pace turns a wait into a conversation. This component fades each word out of a blur as it arrives, trailed by a caret, so the answer feels written rather than pasted.";

export default function StreamingTextDemo() {
  const [done, setDone] = useState(false);
  const [run, setRun] = useState(0);

  return (
    <div className="w-full max-w-xl px-6 py-10">
      <div className="mb-4 flex items-center justify-between">
        <span className="[font-family:var(--font-mono)] text-[0.625rem] font-medium tracking-[0.16em] text-[rgba(var(--ink-rgb),0.35)] uppercase">
          {done ? "● Response complete" : "◌ Streaming…"}
        </span>
        <button
          type="button"
          onClick={() => {
            setDone(false);
            setRun((r) => r + 1);
          }}
          className="cursor-pointer border border-[var(--rule)] px-2.5 py-1 [font-family:var(--font-mono)] text-[0.625rem] font-medium tracking-[0.16em] text-[var(--ink)] uppercase transition-colors hover:bg-[rgba(var(--ink-rgb),0.06)]"
        >
          Replay
        </button>
      </div>
      <StreamingText
        key={run}
        text={ANSWER}
        speed={11}
        startDelay={300}
        onComplete={() => setDone(true)}
        className="text-[0.9375rem] leading-relaxed"
      />
    </div>
  );
}



File location: components/mellow/streaming-text.tsx

File content: "use client";

import React, { useEffect, useMemo, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";

export interface StreamingTextProps {
  /** The full text to stream in. Changing it restarts the stream. */
  text: string;
  /** Reveal granularity. */
  by?: "word" | "char";
  /** Tokens revealed per second. */
  speed?: number;
  /** Delay before streaming starts, in ms. */
  startDelay?: number;
  /** Show the blinking caret while streaming. */
  cursor?: boolean;
  /** Called once when the last token has been revealed. */
  onComplete?: () => void;
  className?: string;
  style?: React.CSSProperties;
}

/**
 * An AI-response text renderer — tokens materialize out of a blur at stream
 * pace, trailed by a blinking caret that vanishes on completion.
 */
export function StreamingText({
  text,
  by = "word",
  speed = 14,
  startDelay = 0,
  cursor = true,
  onComplete,
  className,
  style,
}: StreamingTextProps) {
  const reduced = useReducedMotion();
  const [count, setCount] = useState(0);
  const completedRef = useRef(false);
  const onCompleteRef = useRef(onComplete);
  onCompleteRef.current = onComplete;

  const tokens = useMemo(
    () => (by === "word" ? text.split(/(?<=\s)/) : Array.from(text)),
    [text, by]
  );

  useEffect(() => {
    setCount(0);
    completedRef.current = false;

    let interval: ReturnType<typeof setInterval> | undefined;
    const timeout = setTimeout(() => {
      interval = setInterval(() => {
        setCount((c) => {
          if (c >= tokens.length) return c;
          return c + 1;
        });
      }, 1000 / Math.max(1, speed));
    }, startDelay);

    return () => {
      clearTimeout(timeout);
      if (interval) clearInterval(interval);
    };
  }, [tokens, speed, startDelay]);

  const done = count >= tokens.length;

  useEffect(() => {
    if (done && tokens.length > 0 && !completedRef.current) {
      completedRef.current = true;
      onCompleteRef.current?.();
    }
  }, [done, tokens.length]);

  return (
    <span
      aria-label={text}
      className={[
        "whitespace-pre-wrap [font-family:var(--font-sans)] text-[var(--ink)]",
        className,
      ]
        .filter(Boolean)
        .join(" ")}
      style={style}
    >
      <span aria-hidden="true">
        {tokens.slice(0, count).map((token, i) => (
          <motion.span
            key={i}
            initial={
              reduced ? false : { opacity: 0, y: 4, filter: "blur(4px)" }
            }
            animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
            transition={{ duration: 0.35, ease: "easeOut" }}
            className="inline-block whitespace-pre-wrap"
          >
            {token}
          </motion.span>
        ))}
      </span>
      {cursor && !done && (
        <motion.span
          aria-hidden="true"
          animate={reduced ? { opacity: 1 } : { opacity: [1, 1, 0, 0] }}
          transition={{ duration: 0.9, repeat: Infinity, times: [0, 0.5, 0.5, 1] }}
          className="ml-px inline-block h-[1em] w-[2px] translate-y-[0.15em] bg-[oklch(0.65_0.25_250)]"
        />
      )}
    </span>
  );
}

export default StreamingText;



Usage notes:
Add a StreamingText component from the mellow library — it renders AI responses by revealing tokens at a stream pace, each word fading out of a blur, with a blinking accent caret that disappears on completion. Pass the full `text` (changing it restarts the stream), tune `speed` (tokens/sec) and `by` ('word' | 'char'), and use `onComplete` to flip UI state when the answer finishes. Respects prefers-reduced-motion.

Discovery vocabulary

Related by governed terms

Continue comparing

More from Mellow UI