Source Code

<div class="sr-wrap">
  <div class="sr-card">
    <div class="sr-line" data-target="AI models generate text one token at a time,"></div>
    <div class="sr-line" data-target="predicting each next word from everything that"></div>
    <div class="sr-line" data-target="came before it in the conversation so far."></div>
  </div>
  <button class="sr-run" id="srRun" type="button">Replay stream</button>
</div>

Streaming Text Skeleton Reveal — Token-by-Token Skeleton Shrink CSS JS

Streaming Text Skeleton Reveal · Loaders · Plain HTML, CSS & JS · Live preview

What's included

Features

Skeleton bar genuinely shrinks in width as real words are revealed, not a fade or instant swap
Each line\u2019s starting skeleton width is measured from its true rendered text width
Word-by-word reveal (not character-by-character) matching real streaming chunk granularity
Randomized per-word pause for a natural, non-mechanical pace
Brief highlight color on each newly-revealed word
Strictly sequential multi-line reveal order
Replayable via a dedicated button with fresh randomized timing
Off-screen text measurement respects the line\u2019s real computed font
Zero dependencies — vanilla DOM measurement and CSS transitions
Reusable measureTextWidth() and revealWord() helper functions

About this UI Snippet

Streaming Text Skeleton Reveal \u2014 A Skeleton That Shrinks as Real Text Streams In

Screenshot of the Streaming Text Skeleton Reveal snippet rendered live

Most skeleton loaders for text are static shimmer bars that vanish all at once when the real content arrives. This snippet does something closer to how AI text actually streams in: each line starts as a shimmer bar measured to its final text's real width, and as words are revealed one at a time from the left, the skeleton bar genuinely shrinks from the right by the exact pixel amount the revealed text now occupies \u2014 the placeholder is visibly being consumed by the real content, not replaced by it in one step.

Measuring the true target width first

measureTextWidth() creates a hidden, absolutely-positioned probe span, copies the target line element's real computed font via getComputedStyle(sampleEl).font, sets its text, measures getBoundingClientRect().width, and removes it. This gives each skeleton bar its correct starting width \u2014 matching the actual pixel width the final sentence will occupy in that exact font, not a guessed or fixed placeholder width.

The skeleton shrinks, it doesn't fade

On every word reveal, revealWord() measures the width of the words revealed so far and sets the skeleton's own width to fullWidth - revealedWidth, with a CSS transition: width .25s smoothing each step. Because the revealed text and the shrinking skeleton are laid out inline side by side, the two visibly meet in the middle as words are typed \u2014 real content growing from the left while the remaining unknown length shrinks from the right, until the skeleton reaches zero width and is removed entirely.

Word-by-word, not character-by-character

Unlike the typewriter status log loader, which reveals individual characters, this component reveals whole words at a time with a randomized 140\u2013300ms pause between each \u2014 closer to how streamed AI responses actually arrive over a network connection (chunked by token or word, not by character), and each newly-revealed word gets a brief highlight color before settling to the normal text color.

Sequential multi-line streaming

Multiple lines are revealed strictly in order: runLineSequentially() only starts the next line's word-by-word reveal once the current line's promise chain (driven recursively by revealWord) has fully resolved, mirroring how a real streamed paragraph fills in top to bottom rather than every line typing simultaneously.

Customizing it

Change the data-target attributes to your real placeholder sentences, or better, replace the revealWord recursion with real word-arrival events from your streaming backend \u2014 the width-measurement and skeleton-shrink logic works the same regardless of what triggers each reveal step. Pair it with an AI thinking loader for the state before the first word streams in.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Instead of assuming this is a plain shimmer bar with a fade transition, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how measureTextWidth() uses a hidden off-screen probe and getComputedStyle to determine each line's true target pixel width, and how revealWord() recomputes the skeleton's remaining width on every word reveal so it visibly shrinks in lockstep with the growing real text. The same assistant can help optimize it \u2014 for instance asking whether measuring text width on every single word reveal is expensive enough at scale to warrant caching cumulative widths instead of re-measuring from scratch each time. It's also useful for extending it: ask it to drive the reveal from a real SSE word stream instead of the built-in randomized timer, add a subtle cursor at the boundary between revealed text and remaining skeleton, or support lines whose final length isn't known until streaming completes. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a text skeleton loader in plain HTML, CSS, and JavaScript \u2014 no libraries \u2014 where each line's shimmer skeleton bar genuinely SHRINKS in width as real words stream in, rather than fading out or swapping instantly.

Requirements:
- Several lines, each defined with its eventual full target sentence available up front (e.g. via a data attribute), but initially rendered as a single shimmering skeleton bar.
- Before revealing any words, measure each line's TRUE rendered pixel width for its full target text using a hidden off-screen probe element that copies the real line's computed font (via getComputedStyle), and set the skeleton bar's initial width to that measured value \u2014 not a guessed or fixed placeholder width.
- Reveal the target sentence word by word (not character by character): on each step, append the next word to a growing "revealed" text span on the left side of the line, then re-measure the width of the words revealed so far and shrink the skeleton bar (positioned immediately after the revealed text) to exactly (full width minus revealed width), using a CSS transition on width so the shrink is smooth rather than a snap.
- Space each word reveal apart with a randomized short delay so the pacing feels like real streamed text arriving over a network rather than a fixed metronome.
- Give each newly-revealed word a brief highlight color that settles back to the normal text color shortly after it appears.
- Once a line's skeleton bar reaches zero width, remove it from the DOM entirely.
- Run multiple lines strictly in sequence \u2014 a line's word-by-word reveal must only begin after the previous line has fully finished revealing all of its words.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSThree lines render as shimmer bars sized to their eventual real text width.
  2. 2
    Watch the words stream inEach line reveals words left to right while its skeleton bar shrinks from the right in step.
  3. 3
    Watch lines run in orderThe next line only starts revealing once the previous line finishes.
  4. 4
    Click "Replay stream"Every line resets to its full skeleton width and streams again with new timing.
  5. 5
    Edit data-target textChange each .sr-line\u2019s data-target attribute to your real placeholder sentences.
  6. 6
    Wire up real streamingCall revealWord-equivalent logic from your actual SSE/WebSocket word-arrival events.

Real-world uses

Common Use Cases

AI chat response streaming placeholders
Show a believable shrinking-skeleton effect as real generated text streams in.
Search result summaries with AI overviews
A more literal loading metaphor than a static shimmer bar for streamed summaries.
Live transcription and captioning UIs
Visualize partial text arriving progressively against a known or estimated final length.
Teaching real-width text measurement
A clear example of using a hidden probe element and getComputedStyle to measure text accurately.
Article and blog content placeholders
A richer loading state for paragraphs than plain gray bars, especially over slow connections.
Related: Typewriter Status Log Loader
See the Typewriter Status Log Loader for a character-by-character alternative reveal technique.

Got questions?

Frequently Asked Questions

A normal skeleton is a static bar that vanishes all at once when real content loads. Here, each skeleton bar starts at the real measured width of its final text and shrinks incrementally \u2014 word by word \u2014 as that exact amount of real text is revealed, so the placeholder visibly gets consumed by the arriving content instead of being replaced in one step.

measureTextWidth() renders the full target sentence in a hidden, off-screen probe span that copies the real line element\u2019s computed font via getComputedStyle(sampleEl).font, then reads its getBoundingClientRect().width. That gives an accurate real-world pixel width for the skeleton to start at, matching exactly how long the final sentence will actually render.

Real streamed AI or transcription text typically arrives in word or token-sized chunks over the network, not one character at a time. Revealing whole words with a randomized pause between them more closely matches that real chunk granularity than a character-by-character typewriter effect would.

Replace the recursive revealWord(state, index) calls, which currently advance on a setTimeout, with calls triggered by real word-arrival events from your SSE or WebSocket connection \u2014 each time a new word arrives, append it to state.revealed and shrink state.skel by the newly measured width exactly as revealWord already does.

Because the skeleton is measured from the actual final target string up front, this only becomes a mismatch if you swap in a live streaming source without knowing the eventual full text length. In that case, estimate the skeleton\u2019s starting width from an average character count or a cached previous response length, and let it hit zero width early if the real text finishes revealing sooner than that estimate.

Yes. Keep revealedText and remainingWidth in state per line, measure the full target width once via a hidden ref-based probe element on mount, and recompute remainingWidth as revealedText grows inside your reveal loop \u2014 bind it to the skeleton element\u2019s inline width style exactly as the vanilla version does.