Video Player — Free HTML CSS JS Custom Controls Snippet

Video Player · Layouts · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Progress bar: 3 layers — track / buffer / fill — click-to-seek
timeupdate drives fill width and thumb position
vid.buffered progress indicator updates on buffer events
Hover opacity: controls show on hover, always show when paused (.paused class)
Big play overlay: shows when paused, hides on play
Playback speed: SPEEDS array cycling via speedIdx mod length
Volume range: accent-color:indigo, drives vid.volume and muted state
Spacebar toggle: document keydown, skips INPUT elements

About this UI Snippet

Video Player — Progress Bar, Buffer Indicator, Speed Control, Volume & Fullscreen

Screenshot of the Video Player snippet rendered live

The browser's default video controls are a small grey bar stamped with whatever the OS vendor shipped — Chrome's look different from Safari's, both look different from Firefox's, and none of them match anything you've actually designed. Replace them once with a custom player and you own the entire surface: the font, the colours, the shape of the scrubber thumb, and which controls even appear. This snippet builds a complete replacement from the HTML5 Video API up: a three-layer progress bar with a buffer indicator and click-to-seek, volume slider, mute toggle, playback speed cycling through six presets, fullscreen, a spacebar shortcut that ignores input fields, and controls that fade away while playing and reappear on hover or pause.

Three layers, one click: the progress bar

The scrubber is built from three absolutely-positioned <span> elements stacked inside one container: a dim white track at 20% opacity for the full width, a slightly brighter buffer fill, and an indigo playback fill on top. The seek() function handles a click anywhere on that container — const pct = (e.clientX - rect.left) / rect.width turns a raw pixel position into a 0-to-1 fraction, clamped by Math.max(0, Math.min(1, ...)), then vid.currentTime = pct * vid.duration jumps there immediately. There's no separate "drag" mode — a single click is enough, which keeps the interaction model simple without sacrificing precision.

Reading the buffer without polling

Networks aren't instant: the browser downloads a video in chunks, and a good player tells you how far ahead it has buffered. The progress event fires whenever a new chunk arrives; at that point vid.buffered.end(vid.buffered.length - 1) returns the furthest buffered position in seconds, and dividing by vid.duration converts it to the width percentage for the buffer-fill span. That single read-on-progress pattern is cheaper than a polling loop and exactly as accurate — the browser already knows when data arrives, so there's no reason to ask on a timer.

Controls that appear when you need them, vanish when you don't

The control bar has opacity: 0 and transition: opacity 0.2s by default. Two CSS rules do the rest: .player:hover .controls brings them back whenever a mouse is nearby, and .player.paused .controls keeps them fully visible whenever the video is stopped — because a paused player with invisible controls looks broken. JavaScript only needs to manage the .paused class; the hover state is handled entirely by CSS selectors with no event listeners involved.

Speed cycling as a mod-wrapped index

SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 2] lives in an array; speedIdx tracks the current position. Every click does speedIdx = (speedIdx + 1) % SPEEDS.length, picks the next value, sets vid.playbackRate, and updates the button label. The modulo wraps 2× back to 0.5× without any branching. It's the same compact cycling pattern the Music Player Card uses for its shuffle-and-repeat state — one index, one array, one % operator, done.

A spacebar shortcut that minds its manners

The keydown listener is attached to document, so it fires regardless of where focus sits on the page — press space anywhere and the video toggles. The guard e.target.tagName !== 'INPUT' (extended here to also skip TEXTAREA) prevents the shortcut from firing when a user is typing somewhere on the same page and happens to press the space bar. It's the same "global listener with an escape hatch" pattern behind the keyboard shortcuts modal in the Keyboard Shortcuts Modal snippet.

Swapping in your own video

Update the <source src="..."> with your video URL. For self-hosted files, list a .webm source first (smaller, faster in Chrome and Firefox) with an .mp4 fallback — the browser picks the first format it can play. The poster attribute on <video> sets a thumbnail shown before playback starts and while paused at currentTime = 0; for anything with a representative frame, it's the difference between a player that looks ready and a black rectangle.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Instead of tracing the event wiring by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the buffer indicator is updated from the video's progress event using vid.buffered.end() rather than from timeupdate or a polling interval, and why seek() computes its percentage from getBoundingClientRect() instead of any stored pixel width. It's also worth asking it to reason about the speed-cycling index — have it explain why speedIdx = (speedIdx + 1) % SPEEDS.length is preferable to a chain of if/else comparisons as more speed presets are added. For extending it, have it add chapter markers rendered as small ticks on the progress bar at specific timestamps, picture-in-picture support via requestPictureInPicture, or a captions/subtitles toggle wired to a track element. 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 custom HTML5 video player with fully custom controls in plain HTML, CSS, and vanilla JavaScript with no libraries, replacing every native browser control.

Requirements:
- A video element with multiple source formats, a poster image, and a large centered play/pause overlay icon that hides once playback starts and reappears on pause or when the video ends.
- A progress/seek bar built from three stacked layers: a dim full-width track, a buffer-fill layer, and a playback-fill layer, plus a draggable-looking thumb that only becomes visible on hover. Clicking anywhere on the bar must compute the click position as a fraction of the bar's width using getBoundingClientRect and jump the video's currentTime to that fraction of its duration.
- Update the playback-fill layer's width and the thumb's position on every timeupdate event, and update the buffer-fill layer's width by reading the video's buffered time ranges on the progress event (not on a timer and not on timeupdate).
- A time display showing "current / total" formatted as minutes:seconds, updated alongside the progress fill.
- A volume control combining a mute-toggle button (which must swap its icon and also update the slider's displayed value) and a range slider bound to the video's volume property, where setting the slider to zero should also set the muted property to true.
- A playback speed button that cycles forward through a fixed array of speed multipliers (e.g. 0.5x through 2x) using modulo arithmetic on an index, updating both vid.playbackRate and its own label text on each click.
- A fullscreen toggle button using the Fullscreen API, and control bar visibility that fades in on hover or whenever the video is paused, and fades out otherwise.
- A global spacebar keydown handler that toggles play/pause, but must not fire when the currently focused element is an input field.

Step by step

How to Use

  1. 1
    Click the play button or video area to startThe video starts playing and the controls fade to a lower opacity. Hover over the player to show controls. The space bar also toggles play/pause from anywhere on the page.
  2. 2
    Click the progress bar to seekClick anywhere on the progress bar to jump to that position. The indigo fill shows playback progress; the lighter fill shows how much is buffered.
  3. 3
    Adjust volume and speedDrag the volume slider to set volume. Click the speaker icon to mute/unmute. Click the speed button to cycle through 0.5×, 0.75×, 1×, 1.25×, 1.5×, 2× speeds.
  4. 4
    Replace the video sourceUpdate the <source src="..."> tag with your video URL. Use an MP4 source for widest browser compatibility. Add a WebM source as the first option for better performance in Firefox and Chrome.
  5. 5
    Add a poster imageSet the poster attribute on the <video> element: <video poster="thumbnail.jpg">. The poster shows before the video loads and while it is paused at the start position.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component using useRef for the video element, or "Tailwind" for a React + Tailwind CSS version.

Real-world uses

Common Use Cases

Video course and tutorial platform players
Online learning platforms need custom players that match their design system. The playback speed control (0.5×–2×) is especially valuable for educational content where users review concepts at different speeds.
Product demo and feature showcase video embeds
Replace default browser video controls with a branded player for product demo videos on landing pages. The dark gradient control bar looks professional against any video content.
Onboarding and documentation video guides
Support and documentation sites embed short video guides. The keyboard spacebar shortcut and visible-when-paused controls make the player accessible for users who prefer keyboard navigation.
Custom media library and video management UI
Admin panels and media libraries need players that integrate with their interface. The player's variables (fill width, time display, speed) are all accessible via JavaScript for integration with a playlist or chapter system.
Study HTML5 Video API: timeupdate, buffered, playbackRate
The player demonstrates the core HTML5 Video API: timeupdate for progress, vid.buffered for buffer tracking, vid.playbackRate for speed, vid.requestFullscreen for fullscreen. These APIs power every custom video player on the web.
Portfolio case study and showreel video players
Creative professionals embedding portfolio showreels benefit from a custom player that matches their site's dark theme. The overlay play button and gradient controls look polished on dark and light backgrounds alike.

Got questions?

Frequently Asked Questions

The snippet uses a sample Big Buck Bunny MP4 from Google's CDN. The iframe sandbox may block external resources in some environments. For production, host the video file on your own domain or CDN and set the correct CORS headers. If you need a test video, use a data URI for very short clips, or host locally with a simple HTTP server.

Add chapter data as an array: const chapters = [{time:0,label:"Intro"},{time:120,label:"Main"},{time:300,label:"Summary"}]. After the progress bar, map each chapter to a position marker: const marker = document.createElement("div"); marker.className = "chapter-marker"; marker.style.left = (chapter.time/vid.duration*100)+"%"; progressWrap.appendChild(marker). Style .chapter-marker as a 2px white vertical line.

Add a <track> element inside the <video>: <track kind="subtitles" src="subtitles.vtt" srclang="en" label="English" default>. The browser renders the WebVTT captions automatically. For a custom subtitle display, use vid.textTracks[0].oncuechange to read the current cue text and update a custom overlay element instead of using the native caption rendering.

Click "JSX" to download. Use useRef(null) for vidRef, playerRef, etc. Replace document.getElementById calls with ref.current. Attach event listeners in useEffect: vidRef.current.addEventListener("timeupdate", handleTimeUpdate). Return cleanup: () => vidRef.current.removeEventListener("timeupdate", handleTimeUpdate). For the keyboard listener, attach to document in useEffect with cleanup to prevent multiple listeners.