How to Build the ChatGPT Typing Effect in Modern JavaScript

javatuts1 pts0 comments

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...

copycopied data chatgpt step json const

Related Articles