UI Components Medium
Custom Video Player
A fully customizable HTML5 video player with sleek bespoke controls, progress scrubbing, and volume management.
Open in Lab
MCP
vanilla-js html5 css react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
:root {
--video-accent: #3b82f6;
--video-bg: #000000;
--controls-bg: rgba(0, 0, 0, 0.7);
--controls-text: #ffffff;
}
.video-container {
max-width: 800px;
width: 100%;
margin: 0 auto;
position: relative;
background: var(--video-bg);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
.video-element {
width: 100%;
display: block;
}
.video-controls-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
padding: 1rem;
opacity: 0;
transition: opacity 0.3s ease;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.video-container:hover .video-controls-overlay {
opacity: 1;
}
.progress-container {
width: 100%;
height: 6px;
background: rgba(255, 255, 255, 0.3);
cursor: pointer;
border-radius: 3px;
position: relative;
}
.progress-bar {
width: 100%;
height: 100%;
}
.progress-filled {
height: 100%;
background: var(--video-accent);
width: 0%;
border-radius: 3px;
position: relative;
}
.progress-filled::after {
content: "";
position: absolute;
right: -5px;
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 12px;
background: #fff;
border-radius: 50%;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
display: none;
}
.progress-container:hover .progress-filled::after {
display: block;
}
.controls-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.left-controls,
.right-controls {
display: flex;
align-items: center;
gap: 1rem;
color: white;
}
.control-btn {
background: transparent;
border: none;
color: white;
font-size: 1.25rem;
cursor: pointer;
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.1s;
}
.control-btn:hover {
color: var(--video-accent);
transform: scale(1.1);
}
.time-display {
font-family: monospace;
font-size: 0.875rem;
}
.volume-container {
display: flex;
align-items: center;
gap: 0.5rem;
}
.volume-slider {
width: 60px;
height: 4px;
-webkit-appearance: none;
appearance: none;
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
outline: none;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
background: white;
border-radius: 50%;
cursor: pointer;
}
.speed-select {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
padding: 2px 5px;
border-radius: 4px;
font-size: 0.75rem;
outline: none;
}
.speed-select option {
background: #222;
}const video = document.getElementById("video");
const playPauseBtn = document.getElementById("play-pause");
const playIcon = playPauseBtn.querySelector(".play-icon");
const pauseIcon = playPauseBtn.querySelector(".pause-icon");
const progressFilled = document.getElementById("progress-filled");
const progressContainer = document.querySelector(".progress-container");
const currentTimeEl = document.getElementById("current-time");
const durationEl = document.getElementById("duration");
const muteBtn = document.getElementById("mute-btn");
const volumeSlider = document.getElementById("volume-slider");
const speedSelect = document.getElementById("playback-speed");
const fullscreenBtn = document.getElementById("fullscreen-btn");
const videoContainer = document.getElementById("video-container");
function togglePlay() {
if (video.paused) {
video.play();
playIcon.style.display = "none";
pauseIcon.style.display = "inline";
} else {
video.pause();
playIcon.style.display = "inline";
pauseIcon.style.display = "none";
}
}
function updateProgress() {
const percent = (video.currentTime / video.duration) * 100;
progressFilled.style.width = `${percent}%`;
currentTimeEl.textContent = formatTime(video.currentTime);
}
function formatTime(time) {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
}
function scrub(e) {
const scrubTime = (e.offsetX / progressContainer.offsetWidth) * video.duration;
video.currentTime = scrubTime;
}
function toggleMute() {
video.muted = !video.muted;
muteBtn.querySelector("span").textContent = video.muted ? "🔇" : "🔊";
if (video.muted) {
volumeSlider.value = 0;
} else {
volumeSlider.value = video.volume;
}
}
function handleVolume() {
video.volume = volumeSlider.value;
video.muted = video.volume === 0;
muteBtn.querySelector("span").textContent = video.muted ? "🔇" : "🔊";
}
function handleSpeed() {
video.playbackRate = speedSelect.value;
}
function toggleFullscreen() {
if (!document.fullscreenElement) {
videoContainer.requestFullscreen();
} else {
document.exitFullscreen();
}
}
// Event Listeners
playPauseBtn.addEventListener("click", togglePlay);
video.addEventListener("click", togglePlay);
video.addEventListener("timeupdate", updateProgress);
video.addEventListener("loadedmetadata", () => {
durationEl.textContent = formatTime(video.duration);
});
let mousedown = false;
progressContainer.addEventListener("click", scrub);
progressContainer.addEventListener("mousemove", (e) => mousedown && scrub(e));
progressContainer.addEventListener("mousedown", () => (mousedown = true));
progressContainer.addEventListener("mouseup", () => (mousedown = false));
muteBtn.addEventListener("click", toggleMute);
volumeSlider.addEventListener("input", handleVolume);
speedSelect.addEventListener("change", handleSpeed);
fullscreenBtn.addEventListener("click", toggleFullscreen);
// Hide title overlay on play
video.onplay = () => {
playIcon.style.display = "none";
pauseIcon.style.display = "inline";
};
video.onpause = () => {
playIcon.style.display = "inline";
pauseIcon.style.display = "none";
};<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Video Player</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="video-container" id="video-container">
<video class="video-element" id="video">
<source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
type="video/mp4">
Your browser does not support HTML5 video.
</video>
<div class="video-controls-overlay">
<div class="progress-container">
<div class="progress-bar" id="progress-bar">
<div class="progress-filled" id="progress-filled"></div>
</div>
</div>
<div class="controls-row">
<div class="left-controls">
<button id="play-pause" class="control-btn" aria-label="Play/Pause">
<span class="play-icon">▶</span>
<span class="pause-icon" style="display:none">❚❚</span>
</button>
<div class="time-display">
<span id="current-time">0:00</span> / <span id="duration">0:00</span>
</div>
</div>
<div class="right-controls">
<div class="volume-container">
<button id="mute-btn" class="control-btn" aria-label="Mute/Unmute">
<span class="volume-icon">🔊</span>
</button>
<input type="range" id="volume-slider" min="0" max="1" step="0.1" value="1"
class="volume-slider" />
</div>
<select id="playback-speed" class="speed-select">
<option value="0.5">0.5x</option>
<option value="1" selected>1x</option>
<option value="1.5">1.5x</option>
<option value="2">2x</option>
</select>
<button id="fullscreen-btn" class="control-btn" aria-label="Fullscreen">⛶</button>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useRef, useState, useCallback } from "react";
function formatTime(s: number) {
if (!isFinite(s)) return "0:00";
const m = Math.floor(s / 60);
const sec = Math.floor(s % 60);
return `${m}:${String(sec).padStart(2, "0")}`;
}
export default function VideoPlayerRC() {
const videoRef = useRef<HTMLVideoElement>(null);
const [playing, setPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(1);
const [muted, setMuted] = useState(false);
const [fullscreen, setFullscreen] = useState(false);
const [showControls, setShowControls] = useState(true);
const hideRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
// Use a free sample video
const SRC = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
function showCtrl() {
setShowControls(true);
if (hideRef.current) clearTimeout(hideRef.current);
if (playing) hideRef.current = setTimeout(() => setShowControls(false), 2500);
}
function togglePlay() {
const v = videoRef.current;
if (!v) return;
if (v.paused) {
v.play();
setPlaying(true);
} else {
v.pause();
setPlaying(false);
setShowControls(true);
}
}
function seek(e: React.ChangeEvent<HTMLInputElement>) {
const v = videoRef.current;
if (!v) return;
v.currentTime = Number(e.target.value);
}
function changeVolume(e: React.ChangeEvent<HTMLInputElement>) {
const val = Number(e.target.value);
setVolume(val);
if (videoRef.current) videoRef.current.volume = val;
setMuted(val === 0);
}
function toggleMute() {
const v = videoRef.current;
if (!v) return;
v.muted = !v.muted;
setMuted(v.muted);
}
function toggleFS() {
if (!containerRef.current) return;
if (!document.fullscreenElement) {
containerRef.current.requestFullscreen();
setFullscreen(true);
} else {
document.exitFullscreen();
setFullscreen(false);
}
}
const pct = duration ? (currentTime / duration) * 100 : 0;
return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div
ref={containerRef}
className="relative w-full max-w-2xl bg-black rounded-xl overflow-hidden group"
onMouseMove={showCtrl}
onClick={togglePlay}
>
<video
ref={videoRef}
src={SRC}
className="w-full aspect-video object-cover"
onTimeUpdate={() => setCurrentTime(videoRef.current?.currentTime ?? 0)}
onLoadedMetadata={() => setDuration(videoRef.current?.duration ?? 0)}
onEnded={() => {
setPlaying(false);
setShowControls(true);
}}
muted={muted}
/>
{/* Play overlay */}
{!playing && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="w-16 h-16 rounded-full bg-white/20 flex items-center justify-center backdrop-blur">
<svg width="24" height="24" viewBox="0 0 24 24" fill="white">
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
</div>
</div>
)}
{/* Controls */}
<div
className="absolute bottom-0 left-0 right-0 px-4 py-3 transition-opacity duration-300"
style={{
opacity: showControls ? 1 : 0,
background: "linear-gradient(transparent, rgba(0,0,0,0.8))",
}}
onClick={(e) => e.stopPropagation()}
>
{/* Seek bar */}
<input
type="range"
min={0}
max={duration || 100}
step={0.1}
value={currentTime}
onChange={seek}
className="w-full h-1 mb-2 accent-[#58a6ff] cursor-pointer"
/>
<div className="flex items-center gap-3">
<button
onClick={togglePlay}
className="text-white hover:text-[#58a6ff] transition-colors"
>
{playing ? (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<rect x="6" y="4" width="4" height="16" />
<rect x="14" y="4" width="4" height="16" />
</svg>
) : (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
)}
</button>
<button
onClick={toggleMute}
className="text-white hover:text-[#58a6ff] transition-colors text-xs"
>
{muted ? "🔇" : "🔊"}
</button>
<input
type="range"
min={0}
max={1}
step={0.05}
value={muted ? 0 : volume}
onChange={changeVolume}
className="w-16 h-1 accent-[#58a6ff] cursor-pointer"
/>
<span className="text-white text-xs tabular-nums ml-auto">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
<button
onClick={toggleFS}
className="text-white hover:text-[#58a6ff] text-xs transition-colors"
>
⛶
</button>
</div>
</div>
</div>
</div>
);
}<script setup>
import { ref, computed, onUnmounted } from "vue";
const SRC = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
const videoEl = ref(null);
const containerEl = ref(null);
const playing = ref(false);
const currentTime = ref(0);
const duration = ref(0);
const volume = ref(1);
const muted = ref(false);
const fullscreen = ref(false);
const showControls = ref(true);
let hideTimer = null;
function formatTime(s) {
if (!isFinite(s)) return "0:00";
const m = Math.floor(s / 60);
const sec = Math.floor(s % 60);
return `${m}:${String(sec).padStart(2, "0")}`;
}
function showCtrl() {
showControls.value = true;
if (hideTimer) clearTimeout(hideTimer);
if (playing.value)
hideTimer = setTimeout(() => {
showControls.value = false;
}, 2500);
}
function togglePlay() {
const v = videoEl.value;
if (!v) return;
if (v.paused) {
v.play();
playing.value = true;
} else {
v.pause();
playing.value = false;
showControls.value = true;
}
}
function seek(e) {
const v = videoEl.value;
if (!v) return;
v.currentTime = Number(e.target.value);
}
function changeVolume(e) {
const val = Number(e.target.value);
volume.value = val;
if (videoEl.value) videoEl.value.volume = val;
muted.value = val === 0;
}
function toggleMute() {
const v = videoEl.value;
if (!v) return;
v.muted = !v.muted;
muted.value = v.muted;
}
function toggleFS() {
if (!containerEl.value) return;
if (!document.fullscreenElement) {
containerEl.value.requestFullscreen();
fullscreen.value = true;
} else {
document.exitFullscreen();
fullscreen.value = false;
}
}
function onTimeUpdate() {
if (videoEl.value) currentTime.value = videoEl.value.currentTime;
}
function onLoadedMetadata() {
if (videoEl.value) duration.value = videoEl.value.duration;
}
function onEnded() {
playing.value = false;
showControls.value = true;
}
const pct = computed(() => (duration.value ? (currentTime.value / duration.value) * 100 : 0));
onUnmounted(() => {
if (hideTimer) clearTimeout(hideTimer);
});
</script>
<template>
<div class="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div
ref="containerEl"
class="relative w-full max-w-2xl bg-black rounded-xl overflow-hidden group"
@mousemove="showCtrl"
@click="togglePlay"
>
<video
ref="videoEl"
:src="SRC"
class="w-full aspect-video object-cover"
:muted="muted"
@timeupdate="onTimeUpdate"
@loadedmetadata="onLoadedMetadata"
@ended="onEnded"
/>
<div v-if="!playing" class="absolute inset-0 flex items-center justify-center pointer-events-none">
<div class="w-16 h-16 rounded-full bg-white/20 flex items-center justify-center backdrop-blur">
<svg width="24" height="24" viewBox="0 0 24 24" fill="white"><polygon points="5 3 19 12 5 21 5 3" /></svg>
</div>
</div>
<div
class="absolute bottom-0 left-0 right-0 px-4 py-3 transition-opacity duration-300"
:style="{ opacity: showControls ? 1 : 0, background: 'linear-gradient(transparent, rgba(0,0,0,0.8))' }"
@click.stop
>
<input
type="range"
:min="0"
:max="duration || 100"
:step="0.1"
:value="currentTime"
@input="seek"
class="w-full h-1 mb-2 accent-[#58a6ff] cursor-pointer"
/>
<div class="flex items-center gap-3">
<button @click="togglePlay" class="text-white hover:text-[#58a6ff] transition-colors">
<svg v-if="playing" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
</button>
<button @click="toggleMute" class="text-white hover:text-[#58a6ff] transition-colors text-xs">
{{ muted ? "🔇" : "🔊" }}
</button>
<input
type="range"
:min="0"
:max="1"
:step="0.05"
:value="muted ? 0 : volume"
@input="changeVolume"
class="w-16 h-1 accent-[#58a6ff] cursor-pointer"
/>
<span class="text-white text-xs tabular-nums ml-auto">
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
</span>
<button @click="toggleFS" class="text-white hover:text-[#58a6ff] text-xs transition-colors">⛶</button>
</div>
</div>
</div>
</div>
</template><script>
import { onDestroy } from "svelte";
const SRC = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
let videoEl;
let containerEl;
let playing = false;
let currentTime = 0;
let duration = 0;
let volume = 1;
let muted = false;
let fullscreen = false;
let showControls = true;
let hideTimer = null;
function formatTime(s) {
if (!isFinite(s)) return "0:00";
const m = Math.floor(s / 60);
const sec = Math.floor(s % 60);
return `${m}:${String(sec).padStart(2, "0")}`;
}
function showCtrl() {
showControls = true;
if (hideTimer) clearTimeout(hideTimer);
if (playing)
hideTimer = setTimeout(() => {
showControls = false;
}, 2500);
}
function togglePlay() {
if (!videoEl) return;
if (videoEl.paused) {
videoEl.play();
playing = true;
} else {
videoEl.pause();
playing = false;
showControls = true;
}
}
function seek(e) {
if (!videoEl) return;
videoEl.currentTime = Number(e.target.value);
}
function changeVolume(e) {
const val = Number(e.target.value);
volume = val;
if (videoEl) videoEl.volume = val;
muted = val === 0;
}
function toggleMute() {
if (!videoEl) return;
videoEl.muted = !videoEl.muted;
muted = videoEl.muted;
}
function toggleFS() {
if (!containerEl) return;
if (!document.fullscreenElement) {
containerEl.requestFullscreen();
fullscreen = true;
} else {
document.exitFullscreen();
fullscreen = false;
}
}
function onTimeUpdate() {
if (videoEl) currentTime = videoEl.currentTime;
}
function onLoadedMetadata() {
if (videoEl) duration = videoEl.duration;
}
function onEnded() {
playing = false;
showControls = true;
}
function stopProp(e) {
e.stopPropagation();
}
$: pct = duration ? (currentTime / duration) * 100 : 0;
onDestroy(() => {
if (hideTimer) clearTimeout(hideTimer);
});
</script>
<div class="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
bind:this={containerEl}
class="relative w-full max-w-2xl bg-black rounded-xl overflow-hidden group"
on:mousemove={showCtrl}
on:click={togglePlay}
>
<!-- svelte-ignore a11y-media-has-caption -->
<video
bind:this={videoEl}
src={SRC}
class="w-full aspect-video object-cover"
on:timeupdate={onTimeUpdate}
on:loadedmetadata={onLoadedMetadata}
on:ended={onEnded}
{muted}
/>
{#if !playing}
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
<div class="w-16 h-16 rounded-full bg-white/20 flex items-center justify-center backdrop-blur">
<svg width="24" height="24" viewBox="0 0 24 24" fill="white"><polygon points="5 3 19 12 5 21 5 3" /></svg>
</div>
</div>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="absolute bottom-0 left-0 right-0 px-4 py-3 transition-opacity duration-300"
style="opacity: {showControls ? 1 : 0}; background: linear-gradient(transparent, rgba(0,0,0,0.8))"
on:click={stopProp}
>
<input
type="range"
min={0}
max={duration || 100}
step={0.1}
value={currentTime}
on:input={seek}
class="w-full h-1 mb-2 accent-[#58a6ff] cursor-pointer"
/>
<div class="flex items-center gap-3">
<button on:click={togglePlay} class="text-white hover:text-[#58a6ff] transition-colors">
{#if playing}
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
{:else}
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
{/if}
</button>
<button on:click={toggleMute} class="text-white hover:text-[#58a6ff] transition-colors text-xs">
{muted ? "🔇" : "🔊"}
</button>
<input
type="range"
min={0}
max={1}
step={0.05}
value={muted ? 0 : volume}
on:input={changeVolume}
class="w-16 h-1 accent-[#58a6ff] cursor-pointer"
/>
<span class="text-white text-xs tabular-nums ml-auto">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
<button on:click={toggleFS} class="text-white hover:text-[#58a6ff] text-xs transition-colors">⛶</button>
</div>
</div>
</div>
</div>Custom Video Player
A modern replacement for native browser video controls. This component provides a consistent experience across all browsers with a premium look and feel.
Features
- Custom Play/Pause, Mute toggles
- Progress bar with click-to-seek and hover preview (scrubbing)
- Volume slider control
- Playback speed selection
- Fullscreen support
- Auto-hide controls on inactivity