build-streaming-chat-ui
Builds the chat window for an AI assistant in React - message list, input box, words appearing as they stream in, a stop button, auto-scroll, loading and error states, and safe rendering of the bot's markdown. Use when the user wants a ChatGPT-like chat interface, a chat widget for their site, or streaming AI responses shown on screen.
Version 1.0 · Updated 2026-10-31
Install in Claude Code, Cursor or Codex (optional)
Only for AI coding tools that read skill folders. Run one line in a terminal, in your project folder. It downloads this skill's text files from TheSkillVault and nothing else; you can read them first with “View raw SKILL.md”.
Claude Code (all your projects)
curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/build-streaming-chat-ui/raw' -o ~/'.claude/skills/build-streaming-chat-ui/SKILL.md'Cursor (this project)
curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/build-streaming-chat-ui/raw' -o '.cursor/skills/build-streaming-chat-ui/SKILL.md'Codex and other Agent Skills tools (this project)
curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/build-streaming-chat-ui/raw' -o '.agents/skills/build-streaming-chat-ui/SKILL.md'
Or connect the whole library over MCP
Add TheSkillVault to Cursor or add this to your app's MCP settings (read-only; help):
{
"mcpServers": {
"theskillvault": {
"url": "https://theskillvault.ai/mcp"
}
}
}Build a streaming chat UI
Show the reply word by word as it arrives, so the chat feels fast even when the model is slow.
When to use
- You have a chat API route and need the screen part.
- Replies appear all at once after a long wait, and you want streaming.
- You want a small chat widget in the corner of your site.
Steps
- Hold the state. Keep
messages(role + text),input,status("idle", "streaming" or "error") and anAbortControllerfor stopping. - Send. On submit, add the user's message and an empty assistant message. Then POST the history to
/api/chatwith the controller'ssignal. - Read the stream. Use
res.body.getReader()with aTextDecoder, and append each chunk to the last assistant message. - Stop button. While streaming, show "Stop", which calls
controller.abort(). Keep whatever text already arrived. - Auto-scroll. Scroll to the newest message, but only when the user is already near the bottom.
- States. Disable Send while the input is empty or a reply is streaming. Show a typing indicator before the first chunk. On error, show "Something went wrong - try again" with a Retry button.
- Render safely. Show bot text as plain text, or use a markdown renderer that does not allow raw HTML. Never use
dangerouslySetInnerHTMLon model output. - Accessibility. Give the message list
aria-live="polite", label the input, and let Enter send while Shift+Enter adds a new line.
Example (core of the send function)
async function send(text: string) {
const next = [...messages, { role: "user", content: text }, { role: "assistant", content: "" }];
setMessages(next); setStatus("streaming");
const ctrl = new AbortController(); ctrlRef.current = ctrl;
try {
const res = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ messages: next.slice(0, -1) }), signal: ctrl.signal });
if (!res.ok || !res.body) throw new Error(String(res.status));
const reader = res.body.getReader(); const dec = new TextDecoder();
for (;;) {
const { done, value } = await reader.read(); if (done) break;
const chunk = dec.decode(value, { stream: true });
setMessages((m) => { const c = [...m]; c[c.length - 1] = { ...c[c.length - 1], content: c[c.length - 1].content + chunk }; return c; });
}
setStatus("idle");
} catch (e) { setStatus((e as Error).name === "AbortError" ? "idle" : "error"); }
}
Gotchas
- Model output can contain HTML or script tags. Rendering it as raw HTML is an XSS hole.
- Do not store chats with personal or private data in
localStorageon shared computers without telling users. - React strict mode runs effects twice in development, so do not auto-send from an effect.
Works with any stack
The same pattern works in Vue, Svelte or plain JavaScript. If you use the Vercel AI SDK, its useChat hook handles steps 1-4 instead, but check that its stream format matches your route.
Done when
- Words appear as they stream, and Stop halts them right away.
- A failed request shows an error with Retry, and the page does not freeze.
- A reply containing
<script>shows as text and does not run.
Example prompt
"Build a ChatGPT-style chat window for my app that streams the answer and has a stop button."
Licence: CC BY 4.0. Attribution when you reuse it: From TheSkillVault, https://theskillvault.ai, CC BY 4.0