UI Components Easy
Streaming Text Effect
LLM-style streaming text output that renders characters progressively with a blinking cursor. Configurable speed and delay.
Open in Lab
MCP
vanilla-js react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0d1117;
color: #e6edf3;
min-height: 100vh;
padding: 40px 24px;
display: flex;
justify-content: center;
}
.demo {
width: 100%;
max-width: 600px;
display: flex;
flex-direction: column;
gap: 12px;
}
.section-label {
font-size: 11px;
font-weight: 700;
color: #6c7086;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.stream-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 14px;
padding: 16px 20px;
min-height: 64px;
}
.stream-card--code {
background: #1e1e2e;
border-color: #30363d;
}
.stream-meta {
margin-bottom: 8px;
}
.stream-badge {
display: inline-block;
font-size: 11px;
font-weight: 700;
padding: 3px 10px;
border-radius: 20px;
}
.stream-badge--fast {
background: #dcfce7;
color: #166534;
}
.stream-badge--med {
background: #fef3c7;
color: #92400e;
}
.stream-badge--slow {
background: #fee2e2;
color: #991b1b;
}
.stream-text {
font-size: 14px;
color: #cdd6f4;
line-height: 1.7;
min-height: 24px;
}
.stream-text--code {
font-family: "JetBrains Mono", Menlo, monospace;
font-size: 13px;
color: #a5d6ff;
}
/* Blinking cursor */
.stream-cursor {
display: inline-block;
width: 2px;
height: 1em;
background: #6366f1;
vertical-align: text-bottom;
margin-left: 2px;
animation: blink 1s step-end infinite;
}
.stream-text--code .stream-cursor {
background: #cba6f7;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.controls {
margin-top: 8px;
}
.ctrl-btn {
padding: 10px 24px;
background: #6366f1;
color: #fff;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.ctrl-btn:hover {
opacity: 0.85;
}function streamInto(el, text, charDelay, onDone) {
el.innerHTML = "";
const cursor = document.createElement("span");
cursor.className = "stream-cursor";
el.appendChild(cursor);
let i = 0;
function tick() {
if (i >= text.length) {
cursor.remove();
onDone?.();
return;
}
cursor.insertAdjacentText("beforebegin", text[i++]);
setTimeout(tick, charDelay + Math.random() * (charDelay * 0.5));
}
tick();
}
const TEXTS = {
fast: "The model processes your prompt and generates a response token by token, streaming each word as it becomes available — typically at 80–150 tokens per second.",
med: "Streaming text output creates a sense of speed and responsiveness, even when the actual generation takes several seconds. Users perceive streamed responses as faster.",
slow: "Slow streaming feels deliberate — like the model is thinking carefully before each word.",
code: "const response = await fetch('/api/chat', {\n method: 'POST',\n body: JSON.stringify({ message: userInput }),\n});\n\nconst reader = response.body.getReader();\nconst decoder = new TextDecoder();\n\nwhile (true) {\n const { done, value } = await reader.read();\n if (done) break;\n output += decoder.decode(value);\n}",
};
function start() {
const fast = document.getElementById("streamFast");
const med = document.getElementById("streamMed");
const slow = document.getElementById("streamSlow");
const code = document.getElementById("streamCode");
streamInto(fast, TEXTS.fast, 8);
setTimeout(() => streamInto(med, TEXTS.med, 20), 300);
setTimeout(() => streamInto(slow, TEXTS.slow, 50), 600);
setTimeout(() => streamInto(code, TEXTS.code, 10), 900);
}
start();
document.getElementById("replayBtn").addEventListener("click", start);<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Streaming Text</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h2 class="section-label">Speed variants</h2>
<div class="stream-card">
<div class="stream-meta"><span class="stream-badge stream-badge--fast">Fast (8ms)</span></div>
<p class="stream-text" id="streamFast"></p>
</div>
<div class="stream-card">
<div class="stream-meta"><span class="stream-badge stream-badge--med">Medium (20ms)</span></div>
<p class="stream-text" id="streamMed"></p>
</div>
<div class="stream-card">
<div class="stream-meta"><span class="stream-badge stream-badge--slow">Slow (50ms)</span></div>
<p class="stream-text" id="streamSlow"></p>
</div>
<h2 class="section-label" style="margin-top:28px;">Word-boundary streaming</h2>
<div class="stream-card stream-card--code">
<p class="stream-text stream-text--code" id="streamCode"></p>
</div>
<div class="controls">
<button class="ctrl-btn" id="replayBtn">▶ Replay all</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState, useEffect, useRef } from "react";
const TEXTS = [
"The transformer architecture fundamentally changed natural language processing by introducing the attention mechanism — allowing models to weigh the relevance of each token relative to all others in the sequence, regardless of distance.",
"Retrieval-augmented generation (RAG) combines a parametric language model with a non-parametric retrieval component. At inference time, relevant documents are fetched from a knowledge base and appended to the context, grounding the model's output in factual sources.",
"Constitutional AI trains models to be helpful, harmless, and honest by generating a set of principles the model critiques itself against. This iterative self-improvement loop reduces harmful outputs without requiring human labelers for every edge case.",
];
interface StreamProps {
text: string;
speed?: number;
label?: string;
}
function StreamCard({ text, speed = 16, label }: StreamProps) {
const [displayed, setDisplayed] = useState("");
const [running, setRunning] = useState(false);
const [done, setDone] = useState(false);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const idxRef = useRef(0);
const start = () => {
if (running) return;
idxRef.current = 0;
setDisplayed("");
setDone(false);
setRunning(true);
timerRef.current = setInterval(() => {
idxRef.current++;
setDisplayed(text.slice(0, idxRef.current));
if (idxRef.current >= text.length) {
clearInterval(timerRef.current!);
setRunning(false);
setDone(true);
}
}, speed);
};
const reset = () => {
if (timerRef.current) clearInterval(timerRef.current);
setDisplayed("");
setRunning(false);
setDone(false);
idxRef.current = 0;
};
useEffect(
() => () => {
if (timerRef.current) clearInterval(timerRef.current);
},
[]
);
const pct = Math.round((displayed.length / text.length) * 100);
return (
<div className="bg-[#161b22] border border-[#30363d] rounded-xl overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-4 py-2.5 bg-[#21262d] border-b border-[#30363d]">
<div className="flex items-center gap-2">
<span className="text-[11px] font-bold text-[#8b949e] uppercase tracking-wider">
{label}
</span>
{running && (
<span className="flex items-center gap-1 text-[11px] text-green-400">
<span className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />
Streaming
</span>
)}
{done && <span className="text-[11px] text-[#8b949e]">{text.length} chars</span>}
</div>
<div className="flex gap-1.5">
<button
onClick={start}
disabled={running}
className="px-2.5 py-1 rounded-md text-[11px] font-semibold bg-[#58a6ff]/10 border border-[#58a6ff]/30 text-[#58a6ff] disabled:opacity-30 disabled:cursor-not-allowed hover:bg-[#58a6ff]/20 transition-colors"
>
{done ? "Replay" : "Stream"}
</button>
{(running || done) && (
<button
onClick={reset}
className="px-2.5 py-1 rounded-md text-[11px] font-semibold border border-[#30363d] text-[#8b949e] hover:text-[#e6edf3] transition-colors"
>
Reset
</button>
)}
</div>
</div>
{/* Progress bar */}
{(running || done) && (
<div className="h-0.5 bg-[#21262d]">
<div
className="h-full bg-[#58a6ff] transition-all duration-100"
style={{ width: `${pct}%` }}
/>
</div>
)}
{/* Text */}
<div className="px-5 py-4 min-h-[80px]">
{displayed ? (
<p className="text-[14px] leading-relaxed text-[#cdd6f4]">
{displayed}
{running && (
<span className="inline-block w-0.5 h-4 bg-[#58a6ff] ml-0.5 align-middle animate-pulse" />
)}
</p>
) : (
<p className="text-[13px] text-[#484f58]">Click Stream to start →</p>
)}
</div>
</div>
);
}
export default function StreamingTextRC() {
return (
<div className="min-h-screen bg-[#0d1117] p-6 flex justify-center">
<div className="w-full max-w-[720px] space-y-4">
<StreamCard text={TEXTS[0]} speed={12} label="Standard (12ms)" />
<StreamCard text={TEXTS[1]} speed={4} label="Fast (4ms)" />
<StreamCard text={TEXTS[2]} speed={30} label="Slow (30ms)" />
</div>
</div>
);
}<script setup>
import { reactive, onUnmounted } from "vue";
const TEXTS = [
"The transformer architecture fundamentally changed natural language processing by introducing the attention mechanism — allowing models to weigh the relevance of each token relative to all others in the sequence, regardless of distance.",
"Retrieval-augmented generation (RAG) combines a parametric language model with a non-parametric retrieval component. At inference time, relevant documents are fetched from a knowledge base and appended to the context, grounding the model's output in factual sources.",
"Constitutional AI trains models to be helpful, harmless, and honest by generating a set of principles the model critiques itself against. This iterative self-improvement loop reduces harmful outputs without requiring human labelers for every edge case.",
];
const cards = [
{ text: TEXTS[0], speed: 12, label: "Standard (12ms)" },
{ text: TEXTS[1], speed: 4, label: "Fast (4ms)" },
{ text: TEXTS[2], speed: 30, label: "Slow (30ms)" },
];
const states = reactive(cards.map(() => ({ displayed: "", running: false, done: false })));
const timers = [null, null, null];
const indices = [0, 0, 0];
function start(i) {
if (states[i].running) return;
indices[i] = 0;
states[i].displayed = "";
states[i].running = true;
states[i].done = false;
timers[i] = setInterval(() => {
indices[i]++;
states[i].displayed = cards[i].text.slice(0, indices[i]);
if (indices[i] >= cards[i].text.length) {
clearInterval(timers[i]);
states[i].running = false;
states[i].done = true;
}
}, cards[i].speed);
}
function reset(i) {
if (timers[i]) clearInterval(timers[i]);
indices[i] = 0;
states[i].displayed = "";
states[i].running = false;
states[i].done = false;
}
function pct(i) {
return Math.round((states[i].displayed.length / cards[i].text.length) * 100);
}
onUnmounted(() => {
timers.forEach((t) => {
if (t) clearInterval(t);
});
});
</script>
<template>
<div class="page">
<div class="wrapper">
<div v-for="(card, i) in cards" :key="i" class="card">
<!-- Header -->
<div class="card-header">
<div class="header-left">
<span class="label">{{ card.label }}</span>
<span v-if="states[i].running" class="streaming">
<span class="dot" />
Streaming
</span>
<span v-if="states[i].done" class="chars">{{ card.text.length }} chars</span>
</div>
<div class="header-right">
<button
class="btn-stream"
:disabled="states[i].running"
@click="start(i)"
>
{{ states[i].done ? "Replay" : "Stream" }}
</button>
<button
v-if="states[i].running || states[i].done"
class="btn-reset"
@click="reset(i)"
>Reset</button>
</div>
</div>
<!-- Progress bar -->
<div v-if="states[i].running || states[i].done" class="progress-track">
<div class="progress-bar" :style="{ width: pct(i) + '%' }" />
</div>
<!-- Text -->
<div class="text-area">
<p v-if="states[i].displayed" class="text">
{{ states[i].displayed }}<span v-if="states[i].running" class="cursor" />
</p>
<p v-else class="placeholder">Click Stream to start →</p>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.page {
min-height: 100vh;
background: #0d1117;
padding: 1.5rem;
display: flex;
justify-content: center;
}
.wrapper {
width: 100%;
max-width: 720px;
display: flex;
flex-direction: column;
gap: 1rem;
}
.card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 0.75rem;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.625rem 1rem;
background: #21262d;
border-bottom: 1px solid #30363d;
}
.header-left {
display: flex;
align-items: center;
gap: 0.5rem;
}
.label {
font-size: 11px;
font-weight: 700;
color: #8b949e;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.streaming {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 11px;
color: #4ade80;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #4ade80;
animation: pulse 1.5s ease-in-out infinite;
}
.chars {
font-size: 11px;
color: #8b949e;
}
.header-right {
display: flex;
gap: 0.375rem;
}
.btn-stream {
padding: 0.25rem 0.625rem;
border-radius: 0.375rem;
font-size: 11px;
font-weight: 600;
background: rgba(88, 166, 255, 0.1);
border: 1px solid rgba(88, 166, 255, 0.3);
color: #58a6ff;
cursor: pointer;
transition: background 0.15s;
}
.btn-stream:hover { background: rgba(88, 166, 255, 0.2); }
.btn-stream:disabled { opacity: 0.3; cursor: not-allowed; }
.btn-reset {
padding: 0.25rem 0.625rem;
border-radius: 0.375rem;
font-size: 11px;
font-weight: 600;
border: 1px solid #30363d;
background: transparent;
color: #8b949e;
cursor: pointer;
transition: color 0.15s;
}
.btn-reset:hover { color: #e6edf3; }
.progress-track {
height: 2px;
background: #21262d;
}
.progress-bar {
height: 100%;
background: #58a6ff;
transition: width 0.1s;
}
.text-area {
padding: 1rem 1.25rem;
min-height: 80px;
}
.text {
font-size: 14px;
line-height: 1.6;
color: #cdd6f4;
margin: 0;
}
.cursor {
display: inline-block;
width: 2px;
height: 1rem;
background: #58a6ff;
margin-left: 2px;
vertical-align: middle;
animation: pulse 1.5s ease-in-out infinite;
}
.placeholder {
font-size: 13px;
color: #484f58;
margin: 0;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
</style><script>
import { onDestroy } from "svelte";
const TEXTS = [
"The transformer architecture fundamentally changed natural language processing by introducing the attention mechanism — allowing models to weigh the relevance of each token relative to all others in the sequence, regardless of distance.",
"Retrieval-augmented generation (RAG) combines a parametric language model with a non-parametric retrieval component. At inference time, relevant documents are fetched from a knowledge base and appended to the context, grounding the model's output in factual sources.",
"Constitutional AI trains models to be helpful, harmless, and honest by generating a set of principles the model critiques itself against. This iterative self-improvement loop reduces harmful outputs without requiring human labelers for every edge case.",
];
const cards = [
{ text: TEXTS[0], speed: 12, label: "Standard (12ms)" },
{ text: TEXTS[1], speed: 4, label: "Fast (4ms)" },
{ text: TEXTS[2], speed: 30, label: "Slow (30ms)" },
];
let states = cards.map(() => ({
displayed: "",
running: false,
done: false,
}));
let timers = [null, null, null];
let indices = [0, 0, 0];
function start(i) {
if (states[i].running) return;
indices[i] = 0;
states[i] = { displayed: "", running: true, done: false };
timers[i] = setInterval(() => {
indices[i]++;
states[i].displayed = cards[i].text.slice(0, indices[i]);
states[i] = states[i]; // trigger reactivity
if (indices[i] >= cards[i].text.length) {
clearInterval(timers[i]);
states[i].running = false;
states[i].done = true;
states[i] = states[i];
}
}, cards[i].speed);
}
function reset(i) {
if (timers[i]) clearInterval(timers[i]);
indices[i] = 0;
states[i] = { displayed: "", running: false, done: false };
}
function pct(i) {
return Math.round((states[i].displayed.length / cards[i].text.length) * 100);
}
onDestroy(() => {
timers.forEach((t) => {
if (t) clearInterval(t);
});
});
</script>
<div class="page">
<div class="wrapper">
{#each cards as card, i}
<div class="card">
<!-- Header -->
<div class="card-header">
<div class="header-left">
<span class="label">{card.label}</span>
{#if states[i].running}
<span class="streaming">
<span class="dot" />
Streaming
</span>
{/if}
{#if states[i].done}
<span class="chars">{card.text.length} chars</span>
{/if}
</div>
<div class="header-right">
<button
class="btn-stream"
disabled={states[i].running}
on:click={() => start(i)}
>
{states[i].done ? "Replay" : "Stream"}
</button>
{#if states[i].running || states[i].done}
<button class="btn-reset" on:click={() => reset(i)}>Reset</button>
{/if}
</div>
</div>
<!-- Progress bar -->
{#if states[i].running || states[i].done}
<div class="progress-track">
<div class="progress-bar" style="width: {pct(i)}%;" />
</div>
{/if}
<!-- Text -->
<div class="text-area">
{#if states[i].displayed}
<p class="text">
{states[i].displayed}{#if states[i].running}<span class="cursor" />{/if}
</p>
{:else}
<p class="placeholder">Click Stream to start →</p>
{/if}
</div>
</div>
{/each}
</div>
</div>
<style>
.page {
min-height: 100vh;
background: #0d1117;
padding: 1.5rem;
display: flex;
justify-content: center;
}
.wrapper {
width: 100%;
max-width: 720px;
display: flex;
flex-direction: column;
gap: 1rem;
}
.card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 0.75rem;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.625rem 1rem;
background: #21262d;
border-bottom: 1px solid #30363d;
}
.header-left {
display: flex;
align-items: center;
gap: 0.5rem;
}
.label {
font-size: 11px;
font-weight: 700;
color: #8b949e;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.streaming {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 11px;
color: #4ade80;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #4ade80;
animation: pulse 1.5s ease-in-out infinite;
}
.chars {
font-size: 11px;
color: #8b949e;
}
.header-right {
display: flex;
gap: 0.375rem;
}
.btn-stream {
padding: 0.25rem 0.625rem;
border-radius: 0.375rem;
font-size: 11px;
font-weight: 600;
background: rgba(88, 166, 255, 0.1);
border: 1px solid rgba(88, 166, 255, 0.3);
color: #58a6ff;
cursor: pointer;
transition: background 0.15s;
}
.btn-stream:hover { background: rgba(88, 166, 255, 0.2); }
.btn-stream:disabled { opacity: 0.3; cursor: not-allowed; }
.btn-reset {
padding: 0.25rem 0.625rem;
border-radius: 0.375rem;
font-size: 11px;
font-weight: 600;
border: 1px solid #30363d;
background: transparent;
color: #8b949e;
cursor: pointer;
transition: color 0.15s;
}
.btn-reset:hover { color: #e6edf3; }
.progress-track {
height: 2px;
background: #21262d;
}
.progress-bar {
height: 100%;
background: #58a6ff;
transition: width 0.1s;
}
.text-area {
padding: 1rem 1.25rem;
min-height: 80px;
}
.text {
font-size: 14px;
line-height: 1.6;
color: #cdd6f4;
margin: 0;
}
.cursor {
display: inline-block;
width: 2px;
height: 1rem;
background: #58a6ff;
margin-left: 2px;
vertical-align: middle;
animation: pulse 1.5s ease-in-out infinite;
}
.placeholder {
font-size: 13px;
color: #484f58;
margin: 0;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
</style>Streaming text component that renders content character-by-character like a real LLM response. Supports variable speed, word-boundary streaming, blinking cursor, and multiple simultaneous streams.