webdevpuneet.com

Video Buffering Overlay — Player Chrome Buffering Spinner

Video Buffering Overlay · Loaders · Plain HTML, CSS & JS · Live preview

CategoryLoaders

Copy & paste

Source Code

<div class="vb-player" id="vbPlayer">
  <div class="vb-frame">
    <div class="vb-scene"></div>
    <div class="vb-buffer-overlay" id="vbOverlay">
      <div class="vb-ring">
        <span></span><span></span><span></span>
      </div>
    </div>
  </div>

  <div class="vb-controls">
    <button type="button" class="vb-play" id="vbPlay" aria-label="Play">▶</button>
    <div class="vb-track">
      <div class="vb-buffered" id="vbBuffered"></div>
      <div class="vb-played" id="vbPlayed"></div>
      <div class="vb-scrubber" id="vbScrubber"></div>
    </div>
    <span class="vb-time" id="vbTime">0:00 / 2:41</span>
    <button type="button" class="vb-mute" aria-label="Mute">🔊</button>
  </div>
</div>
<button type="button" class="vb-demo" id="vbDemo">Simulate a rebuffer</button>

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

Playback-vs-buffer driven overlay
The spinner appears exactly when playback consumes all buffered data.
Two-range scrubber
Separate played and buffered fills, matching real video player scrubbers.
Player-styled chrome
Play/pause, scrubber, timestamp, and mute controls, not a bare page loader.
Scoped scrim overlay
A dark semi-transparent scrim over just the video frame, not the whole page.
Orbiting three-dot ring
The recognisable video-platform buffering indicator, centered on the frame.
Auto-resolving stall
Buffering clears itself once simulated runway rebuilds, no fixed timer.
On-demand stall trigger
A demo control forces a rebuffer instantly for testing the transition.
Native-event-ready structure
Maps directly onto a real <video>'s waiting/playing events.

About this UI Snippet

Video Buffering Overlay — A Spinner Driven by the Playback/Buffer Relationship

Screenshot of the Video Buffering Overlay snippet rendered live

A generic page spinner communicates "wait," full stop. A video buffering overlay needs to communicate something more specific: playback has caught up to how much has actually downloaded, and it will resume the instant enough new data arrives — the same distinction YouTube, Netflix, and every serious video player make between an initial load and a mid-playback stall. This snippet builds real player chrome — a scene area, a scrubber with separate played and buffered ranges, play/mute controls — and drives its centred buffering spinner directly off the relationship between a simulated playback position and a simulated buffered position, not a fixed timer.

A scrubber with two independent ranges

The track shows two overlapping fills: .vb-played (how far playback has progressed) and .vb-buffered (how much has downloaded ahead of that, the lighter grey range players like YouTube show past the red played bar). These are two separate widths driven by two separate numbers (current and bufferedAhead), which is what lets the buffering logic be meaningful rather than decorative — a real player computes exactly this same gap to decide whether it needs to stall.

The overlay appears exactly when playback catches the buffer

tick() runs once a second and only advances current if it's still behind bufferedAhead. The moment current >= bufferedAhead — playback has consumed everything that's downloaded — startBuffering() fires: the spinner overlay fades in over the frozen scene, and a separate faster interval grows bufferedAhead (simulating the network catching up) until there's enough runway ahead of the current position again, at which point the overlay fades out and normal playback resumes. This mirrors the real waiting/playing events a <video> element fires — the overlay is a direct visualisation of the playback-versus-download race, not a spinner shown for a fixed duration.

Styled as player chrome, not a page loader

The buffering ring sits centred over a scene area styled like dark ambient video content, above the transport controls (play/pause, scrubber, timestamp, mute) rather than floating over the whole page. The overlay itself is a semi-transparent black scrim (rgba(0,0,0,.5)) rather than the light backdrop-blur of a page-level loading overlay, because the convention for video players is a darkened frame with three orbiting dots, immediately recognisable as "your video, waiting" rather than "the whole app, waiting."

A demo control to force a stall on demand

A "Simulate a rebuffer" button snaps bufferedAhead down to just ahead of current, so the very next tick() triggers startBuffering() immediately — useful for seeing the overlay's fade-in and the scrubber's buffered range visibly regrow without waiting for natural playback to catch up on its own.

Wiring it to a real <video> element

Replace the simulated current/bufferedAhead numbers with a real <video> element's currentTime and its buffered TimeRanges object, and swap the manual tick()/startBuffering() calls for the element's native waiting and playing (or canplay) events — show the overlay on waiting, hide it on playing. The scrubber, controls, and overlay CSS all carry over unchanged. Pair it with a loading overlay for the page shell around the player.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA dark video player renders, already "playing" with a growing scrubber.
  2. 2
    Let it runPlayback eventually catches up to the buffered edge and the spinner appears automatically.
  3. 3
    Watch it resolveThe buffered range refills faster than playback, then the overlay fades and playback resumes.
  4. 4
    Click "Simulate a rebuffer"Force the stall immediately to see the overlay without waiting.
  5. 5
    Toggle play/pausePlayback (and the tick loop) pauses; buffering logic only runs while playing.
  6. 6
    Wire a real videoSwap the simulated numbers for a real <video>'s currentTime/buffered and waiting/playing events.

Real-world uses

Common Use Cases

Video streaming platforms
Show the spinner only when playback catches the buffered edge, following the convention viewers already know from major streaming sites.
Course and lesson players
Reveal true stall states during an e-learning video, with separate played and buffered fills on the scrubber.
Live stream viewers
Communicate rebuffering distinctly from the initial load, with a scrim covering only the video frame and not the whole page.
Embedded product demos
Give marketing pages inline demo videos real player chrome, including play and pause, scrubber, timestamp and mute controls.
Custom video element UIs
Use it as a reference for bufferedAhead, where the overlay hides again once enough runway has rebuilt ahead of the playhead.
Related: Full-Screen Percentage Counter Loader
See the Full-Screen Percentage Counter Loader for a related loaders pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

tick() only advances the current playback position while it remains behind bufferedAhead. The instant current catches up to bufferedAhead — meaning playback has consumed everything that's downloaded — startBuffering() fires. This mirrors a real video element's waiting event, which fires for exactly the same reason: the browser has run out of buffered data to play.

played (an indigo fill) tracks how far the user has actually watched, driven by current. buffered (a lighter grey fill) tracks how much has downloaded ahead of that, driven by bufferedAhead — the same range shown as a lighter grey bar past the red played indicator on YouTube. They're two independent values, which is what makes it possible to detect and visualize the moment playback outruns the download.

Once startBuffering() fires, a separate faster interval grows bufferedAhead well ahead of the normal playback rate (simulating the network catching up), checking after each tick whether there's now more than 8 seconds of runway between bufferedAhead and current. Once that gap is large enough (or the video is fully buffered), the interval clears itself and the overlay fades out — no fixed "spin for N seconds" timer decides when it ends.

Video platforms consistently scope the buffering indicator to the player itself — a darkened video frame with a centered spinner — so the rest of the page (comments, related videos, navigation) stays fully usable and unobscured while only the video is waiting. A full-page loading overlay would incorrectly suggest the entire app, not just the video, is stalled.

Replace the simulated current and bufferedAhead numbers with the real element's currentTime property and its buffered TimeRanges (video.buffered.end(0) for the nearest buffered range), and listen for the element's native waiting and playing (or canplay) events to show and hide the overlay, instead of computing the catch-up condition manually in tick(). The scrubber math, controls, and overlay CSS all carry over unchanged.