webdevpuneet.com

Live Caption Overlay — Free HTML CSS JS Snippet

Live Caption Overlay · Media · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<div class="phone">
  <div class="phone-notch"></div>
  <div class="video-area">
    <div class="video-fake">
      <div class="video-blob blob-1"></div>
      <div class="video-blob blob-2"></div>
      <div class="rec-badge"><span class="rec-dot"></span>REC</div>
    </div>
  </div>

  <div class="caption-overlay">
    <div class="transcript-log" id="transcript-log"></div>
    <div class="live-line-wrap">
      <svg class="mic-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v1a7 7 0 0 1-14 0v-1M12 18v4"/></svg>
      <div class="live-line" id="live-line" aria-live="polite"></div>
    </div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Word-by-word reveal driven by chained setTimeout calls, not a fixed-interval timer that can drift
Three-state per-word model (unspoken / current / spoken) mapped directly to CSS classes
Karaoke-style highlight sweep using a scaleX(0) to scaleX(1) pseudo-element animation
Per-word variable timing (short words pause less, punctuation-adjacent words pause more) for natural cadence
Auto-scrolling transcript log capped at three lines with a fade-mask gradient on older entries
breakAfter-flagged script entries trigger line commits, simulating sentence-boundary detection
aria-live="polite" on the live caption line for screen-reader announcement of new text
Fully self-contained script-driven simulation designed to be swapped for a real SpeechRecognition result stream

About this UI Snippet

Live Caption Overlay — Word-by-Word Karaoke Highlight State Machine & Auto-Scrolling Transcript in Vanilla JS

Screenshot of the Live Caption Overlay snippet rendered live

Live captioning UIs — the bar in a video call, the OS-level "Live Caption" feature, or a livestream's auto-generated subtitles — share a distinct visual language: words appear one at a time rather than the whole sentence popping in at once, the word currently being spoken is visually emphasized, already-said words fade back so the eye is drawn to what is new, and older lines scroll up and out to make room. This snippet reproduces that exact behavior using a scripted array standing in for a real speech recognizer, a small per-word state machine, and CSS-driven highlight and scroll animations — no video call SDK, no actual audio, no external captioning service.

The three-state word model

Every word in the currently-forming caption line carries one of three states, tracked directly on a plain JS object: unspoken (the default, dimmest, meaning "not reached yet" — though in this simulation words are only added to the array once reached, so this state mostly matters for words that just finished their sweep), current (the single word actively being "spoken," rendered at full brightness with the highlight sweep), and spoken (previously-current words, faded to a middle opacity so they remain legible but clearly de-emphasized). renderLiveLine() maps this state directly to a CSS class per word — .word, .word.current, or .word.spoken — so the visual treatment is entirely declarative CSS, not manually toggled inline styles.

The karaoke sweep

The .word.current::after pseudo-element is an absolutely positioned bar sitting just under the word's baseline, given a horizontal gradient and animated from scaleX(0) to scaleX(1) with transform-origin: left over 0.55s. Because scaleX animates the pseudo-element's own width rather than the word's opacity or color, it reads as a highlight physically sweeping left-to-right underneath the word — the same visual metaphor karaoke apps and Instagram/TikTok caption styles use to show exactly which syllable is "now."

Word timing via a scripted array

SCRIPT is an ordered array of { text, pause, breakAfter } objects. step() advances through it with plain setTimeout chains rather than setInterval: each word's pause value is the delay before the NEXT word appears, which lets the simulation vary its own cadence — short words like "the" get a quick 180ms pause, longer or more emphatic words get 300-500ms — mimicking the uneven rhythm of real speech far better than a fixed-interval timer would. Chaining individual setTimeout calls (each one scheduling the next only after it fires) also means the timing never drifts the way a naive setInterval can under tab-throttling or long task queues, since each step only ever waits relative to when the previous one actually completed.

Line breaks and the scrolling transcript

Some script entries carry breakAfter: true, marking a natural sentence or clause boundary. When step() reaches one, after that word's own pause it calls commitLine(), which joins the accumulated currentWords into one string, appends it as a new .transcript-line div above the live line, sets transcriptLog.scrollTop to its scrollHeight so it auto-scrolls to reveal the newest line, and trims old entries once more than three lines have accumulated so the log never grows unbounded. The live line then empties and a short 260ms pause plays before the next line starts building, giving a small visual breath between clauses the way a real captioner's paragraph breaks do. A CSS mask-image gradient over the whole transcript-log container fades older (visually higher, scrolled-past) lines toward transparent, reinforcing the sense that they are receding rather than just sitting in a plain list.

Swapping in the real Web Speech API

To replace the simulated array with an actual live transcript, create a SpeechRecognition (or webkitSpeechRecognition) instance with continuous = true and interimResults = true, and listen for its result event. Each result carries a list of alternatives; the isFinal flag on a result distinguishes a settled transcript chunk from an interim, still-changing guess. In place of step()'s scripted word-by-word loop, you would split the interim transcript's latest text delta into words as they arrive, push each new word into currentWords with state: 'current' (demoting the previous current word to spoken, exactly as this snippet already does), and call commitLine() whenever a result arrives with isFinal true rather than relying on a hand-authored breakAfter flag. The rendering, state model, and transcript-scrolling code all carry over unchanged — only the source of "what word arrived next and when" needs to be swapped from the SCRIPT array to the recognizer's event stream.

Step by step

How to Use

  1. 1
    Watch words appear one at a time in the live lineA scripted sentence plays back word by word at varying speed, each new word appearing at full brightness with a gradient sweep animating underneath it.
  2. 2
    Notice the previous word dim as the next one arrivesAs soon as a new word becomes current, the previously-current word fades to a lower opacity — the classic already-spoken look of a live captioning bar.
  3. 3
    See a full clause slide into the transcript log aboveWhen a sentence or clause finishes, the whole line moves up into a small scrolling history above the live line, and the live line clears to start the next one.
  4. 4
    Watch older transcript lines fade near the topA soft gradient mask fades the topmost, oldest lines toward transparent as new lines push in from below, keeping the log from feeling like a static list.
  5. 5
    Watch the cycle repeatOnce the scripted sentence finishes, the whole simulation pauses briefly and restarts from the beginning, so the pattern is easy to observe on loop.

Real-world uses

Common Use Cases

Video call and conferencing caption bars
Reproduce the live captions overlay found in Zoom, Google Meet, and Microsoft Teams, useful alongside a mobile chat screen mockup when prototyping a full communications app.
MEDIA
Livestream and video accessibility overlays
Simulate auto-generated subtitles for a livestream or recorded video demo, pairing naturally with a video player or the phone mockup here as a self-contained accessibility feature showcase.
Teaching Web Speech API integration
Use the scripted word array as a stand-in you can study and swap for a real SpeechRecognition result stream, making this a hands-on teaching example for browser speech-to-text without needing a live microphone during a demo or class.
OS-level live-caption feature prototypes
Mock up an "always-on" system captioning feature (similar to Android and macOS Live Caption) for a design proposal or accessibility feature pitch without wiring up real audio capture.
Portfolio pieces demonstrating timing and state-machine design
A compact showcase of chained-timeout sequencing and per-item state transitions — concepts that transfer directly to toast queues, countdown timers, and other timed UI sequences.
Related: Dialer Keypad
See the Dialer Keypad for a related mobile pattern worth pairing with this one.
Related: Keyboard-Safe Fixed Input Bar with the VisualViewport API
See the Keyboard-Safe Fixed Input Bar with the VisualViewport API for a related mobile pattern worth pairing with this one.
Related: Mobile OTP Verification Screen
See the Mobile OTP Verification Screen for a related mobile pattern worth pairing with this one.
Related: Mobile Permission Request Screen
See the Mobile Permission Request Screen for a related mobile pattern worth pairing with this one.
Related: Mobile Split Bill Screen
See the Mobile Split Bill Screen for a related mobile pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

setInterval fires on a fixed period regardless of how long the previous tick took to run, so under load (or when a browser tab is throttled in the background) its ticks can bunch up or drift out of sync with real elapsed time. Chaining individual setTimeout calls, where each one only schedules the next after the current word has finished rendering, guarantees each pause is measured relative to the actual previous step rather than an absolute clock — and it also lets every word have its own distinct pause duration, which a single fixed setInterval period cannot express.

Use the Web Speech API: const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)(); set recognition.continuous = true and recognition.interimResults = true, then listen to the result event. Each event.results entry has an isFinal flag; treat interim (isFinal false) text as still-arriving words you push into currentWords with state "current" the same way step() does, and call commitLine() when a result's isFinal becomes true instead of relying on a breakAfter flag. The rendering and transcript-scrolling logic need no changes at all.

The mask-image: linear-gradient(...) applied to .transcript-log fades content near the top of the container toward transparent continuously, so lines feel like they are receding into history rather than abruptly vanishing. The actual DOM removal (transcriptLog.removeChild) only happens once more than three lines have accumulated, which is a separate, purely structural cleanup step from the visual fade.

Yes. In React, move the SCRIPT-stepping setTimeout chain into a useEffect, store the pending timeout id in a ref, and call clearTimeout on that ref in the effect's cleanup function so a fast unmount does not leave a stray timer trying to update unmounted state. In Vue, start the chain in onMounted and clear the pending timeout in onUnmounted. In Angular, start it in ngAfterViewInit and clear it in ngOnDestroy. If you wire up the real SpeechRecognition API instead of the simulated script, also call recognition.stop() in the same cleanup hook so the microphone is released when the component unmounts.

Restructure SCRIPT so each entry is a single character with a very short pause (roughly 30-60ms) instead of a whole word, and remove the space-joining logic in renderLiveLine and commitLine since characters concatenate directly. Note this changes the visual metaphor from "live captioning" (which is always word-granular, matching how speech recognizers emit results) to a typewriter effect, which reads differently and is a distinct, separately-documented UI pattern.