html video

HTML Video — Custom Controls & Media API

5 interactive examples

Media API event explorer

Use the player below. Event chips light up as the HTMLMediaElement fires them. The property readout updates live — this is the API surface every custom control reads from.

0:00 / 0:30
loadedmetadata
play
pause
timeupdate
progress
seeking
volumechange
ratechange
ended
currentTime0.00
duration30.00
pausedtrue
buffered.end0.00
--:--:--Play the video — watch events fire and properties update

Complete custom player — all controls wired to the Media API

A full player built from scratch: play/pause, seek, volume, mute, playback speed. Every control reads and writes the media element. Click the video to play/pause.

0:00 / 0:30
--:--:--Use the controls — each action calls the corresponding Media API property

The buffered progress bar — TimeRanges, not a number

This big progress bar shows both regions: grey = buffered (downloaded), red = played (watched). Seek around to create multiple buffered ranges and watch the TimeRanges object update.

0:00 / 0:30
0:00● played● buffered0:30
video.buffered → length: 0
// buffered is a TimeRanges object, NOT a number function getBufferedEnd() { for (let i = 0; i < video.buffered.length; i++) { if (video.buffered.start(i) <= video.currentTime && video.currentTime <= video.buffered.end(i)) { return video.buffered.end(i); // grey bar width } } }
--:--:--Play, then seek ahead to create gaps in the buffered ranges

Captions via the textTracks API + ::cue styling

Play the video — captions appear from a simulated WebVTT track. Switch the track mode and restyle the cues live. The active cue highlights in the VTT source.

caption text
0:00 / 0:30
track.mode
showing: captions visible and active
::cue styling (live)
captions-en.vtt
--:--:--Play to see captions · toggle CC · change mode · restyle cues

Picture-in-Picture & Fullscreen APIs

Both are Promise-based and need feature detection. The simulated player demonstrates the state transitions and events each API fires.

0:00 / 0:30

Picture-in-Picture

document.pictureInPictureEnabledtrue
requestPictureInPicture() returns a Promise
Events: enterpictureinpicture, leavepictureinpicture

Fullscreen

Fullscreen the container, not the video
Safari: webkitRequestFullscreen
iOS: video.webkitEnterFullscreen()
// Both APIs are Promise-based — await + try/catch pipBtn.addEventListener('click', async () => { try { if (document.pictureInPictureElement) await document.exitPictureInPicture(); else await video.requestPictureInPicture(); } catch (e) { console.error(e); } });
--:--:--Click the PiP (⧉) and Fullscreen (⛶) buttons to see the state changes
Read the tutorial