How to Build the ChatGPT Typing Effect in Modern JavaScript<br>JavaScript Development Space
Follow Us<br>TelegramBskySubstackDailyDev
-->Search posts, tutorials, and more...Ctrl+K<br>Ctrl+K
Quick Access
All PostsBrowse our latest articles
How-to GuidesStep-by-step tutorials
Code SnippetsReusable code examples
Friday LinksWeekly curated content
No results found<br>Try different keywords or check the spelling
One of the reasons ChatGPT feels so responsive has nothing to do with model speed.
The answer isn’t generated instantly. Instead, text starts appearing almost immediately, making the application feel alive. Even when the model needs ten seconds to finish a response, users never feel like they’re staring at a frozen screen.
Many tutorials call this a “typewriter effect.”
It isn’t.
ChatGPT isn’t animating existing text. It’s rendering new tokens the moment they arrive from the server.
Let’s build the real thing.
Why ChatGPT Feels Fast
A traditional API waits until everything is finished before returning a response.
plaintext
CopyCopied!<br>Browser<br>├──── Request ───► Server<br>│ Waiting...<br>◄──── Full JSON ──┤
Streaming works differently.
plaintext
CopyCopied!<br>Browser<br>├──── Request ───► Server<br>◄──── Hello<br>◄──── there<br>◄──── this<br>◄──── is<br>◄──── ChatGPT...
The model is already generating tokens internally.
Streaming simply exposes those tokens as soon as they exist.
That small UX improvement completely changes how the application feels.
Step 1. Enable Streaming
Most AI APIs expose a streaming option.
js
CopyCopied!<br>const response = await fetch("/api/chat", {<br>method: "POST",<br>headers: {<br>"Content-Type": "application/json",<br>},<br>body: JSON.stringify({<br>messages,<br>stream: true,<br>}),<br>});
Instead of waiting for one large JSON object, the server keeps the HTTP connection open and continuously sends small chunks of data.
Step 2. Read the Response Stream
This is where most tutorials stop.
js
CopyCopied!<br>const reader = response.body.getReader();
while (true) {<br>const { value, done } = await reader.read();
if (done) break;
console.log(value);
Unfortunately, this doesn’t produce a polished ChatGPT experience.
There are several problems waiting for you.
Step 3. Decode UTF-8 Correctly
The biggest mistake is decoding every chunk independently.
Chinese, Japanese, Korean, emoji, and many Unicode characters occupy multiple bytes.
A chunk may split one character into two different packets.
Bad:
js
CopyCopied!<br>decoder.decode(value);
Correct:
js
CopyCopied!<br>const decoder = new TextDecoder();
text += decoder.decode(value, {<br>stream: true,<br>});
The stream: true option tells the decoder to keep incomplete characters in memory until the remaining bytes arrive. Without it, you’ll eventually see corrupted text.
Step 4. Keep Your Own Buffer
Another mistake is assuming every network packet contains complete JSON.
It doesn’t.
One read may end halfway through a message.
plaintext
CopyCopied!<br>data: {"choices":[{"delta":{"cont
next chunk...
ent":"Hello"}}]}
Instead, accumulate everything inside a buffer.
js
CopyCopied!<br>let buffer = "";
buffer += decoder.decode(chunk, {<br>stream: true,<br>});
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
Only parse complete lines.
Leave incomplete data for the next iteration.
Step 5. Parse Server-Sent Events
Most LLM providers send responses like this:
plaintext
CopyCopied!<br>data: {...}
data: {...}
data: {...}
data: [DONE]
Extract only the useful content.
js
CopyCopied!<br>for (const line of lines) {<br>if (!line.startsWith("data:")) {<br>continue;
const json = line.slice(5).trim();
if (json === "[DONE]") {<br>break;
const data = JSON.parse(json);
const token =<br>data.choices?.[0]?.delta?.content;
if (token) {<br>output += token;
At this point you’ll already have working streaming.
But it still won’t feel exactly like ChatGPT.
Step 6. Update the DOM Efficiently
Many developers do this:
js
CopyCopied!<br>element.textContent += token;
Every token forces the browser to repaint.
If the response contains thousands of tokens, performance starts dropping.
Instead, accumulate tokens.
js
CopyCopied!<br>let pending = "";
pending += token;
Then render only once per animation frame.
element.textContent += pending;
pending = "";<br>scheduled = false;<br>});<br>}" data-astro-cid-5zfle2qk>js
CopyCopied!<br>let scheduled = false;
function flush() {<br>if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {<br>element.textContent += pending;
pending = "";<br>scheduled = false;<br>});
This keeps scrolling smooth even during very long responses.
Step 7. Auto-Scroll Like ChatGPT
Users should never have to scroll manually while new text appears.
js
CopyCopied!<br>function scrollToBottom() {<br>container.scrollTop =<br>container.scrollHeight;
Call it after every rendered batch instead of every token.
The movement becomes smooth and natural.
Step 8. Markdown Should Render While Streaming
ChatGPT formats text immediately.
Headings appear instantly.
Lists grow line by line.
Code...