Signature
An animated SVG signature effect that draws out text as if hand-written.
Live preview recorded from Mellow UI; component and demo published by Mellow UI. Preview: platform recorded · rights authorized.
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 UIWhy 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.
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
An animated SVG signature effect that draws out text as if hand-written.
More from Mellow UI