You Might Also Like
Video Buffering Overlay — Player Chrome Buffering Spinner
Video Buffering Overlay · Loaders · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Video Buffering Overlay — A Spinner Driven by the Playback/Buffer Relationship

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.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how tick() detects that playback has caught up to the buffered edge (current >= bufferedAhead) and why that specific condition is the correct simulation of a real <video> element's waiting event, versus just showing a spinner for a fixed number of seconds. It's worth asking how to map this onto a real video element too: which native events (waiting, playing, canplay) correspond to startBuffering() firing and the overlay clearing, and how to read the actual buffered TimeRanges object instead of the simulated bufferedAhead number. For extending it, ask for a version that also shows a small percentage or "low bandwidth" hint during a prolonged stall, adaptive-bitrate-style logic that lowers simulated quality after repeated rebuffers, or a way to visually distinguish an initial load buffering state from a mid-playback rebuffer. 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:
Build a video player interface in plain HTML, CSS, and JavaScript with a buffering overlay that appears based on the relationship between simulated playback progress and simulated buffered progress — not a fixed-duration spinner.
Requirements:
- Player chrome consisting of a video frame area (a styled placeholder "scene" is fine, no real video file required), a scrubber track showing two independent overlapping fills — one representing how far playback has progressed and one representing how much has been buffered ahead of that — a play/pause button, and a timestamp label.
- A recurring playback tick (e.g. once per second while "playing") that advances the playback position only if it remains behind the buffered position; the moment the playback position reaches or exceeds the buffered position, trigger a distinct buffering state instead of allowing playback to advance further.
- The buffering state must show a centered spinner overlay scoped to just the video frame (a semi-transparent dark scrim, not a full-page overlay) while playback is frozen, and must run a separate faster process that increases the buffered position until there is a meaningful amount of runway ahead of the current playback position again, at which point the overlay must automatically clear and normal playback resumes — no fixed "wait N seconds" timer may decide when buffering ends.
- Add a button that manually forces the buffered position down to just ahead of the current playback position, so a rebuffer can be triggered on demand for demonstration without waiting for natural playback to catch up.
- The play/pause button must correctly pause the recurring playback tick when paused, and buffering logic must not run while playback is paused.
- Style the buffering spinner as a small orbiting multi-dot ring centered on the video frame, matching the visual convention used by real video streaming platforms, distinct from a generic full-page loading spinner.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
- 1Paste HTML, CSS, and JSA dark video player renders, already "playing" with a growing scrubber.
- 2Let it runPlayback eventually catches up to the buffered edge and the spinner appears automatically.
- 3Watch it resolveThe buffered range refills faster than playback, then the overlay fades and playback resumes.
- 4Click "Simulate a rebuffer"Force the stall immediately to see the overlay without waiting.
- 5Toggle play/pausePlayback (and the tick loop) pauses; buffering logic only runs while playing.
- 6Wire a real videoSwap the simulated numbers for a real <video>'s currentTime/buffered and waiting/playing events.
Real-world uses
Common Use Cases
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.