Video Player Codepen: Custom Html5
Beyond the Native: The Art and Engineering of Custom HTML5 Video Players
In the early days of the web, video was a siloed experience, reliant on third-party plugins like Flash or QuickTime. With the advent of HTML5, the <video> tag democratized media embedding, making it as native as an image or a paragraph. However, while the functionality became native, the default user interface provided by browsers—often a utilitarian set of gray controls—remained visually rigid and functionally limited. This limitation birthed a thriving genre of front-end development tutorials and "CodePen challenges": the custom HTML5 video player. Building a custom player is more than an aesthetic exercise; it is a deep dive into the intersection of UI/UX design, JavaScript event handling, and the accessibility requirements of modern web applications.
The Deconstruction of Native Controls
The primary motivation for a custom player is control. A CodePen demonstration of a video player typically begins by stripping the browser of its authority. The developer adds the controls attribute to the HTML tag only to realize that to build something new, one must first destroy the old. By setting controls="false" (or omitting the attribute entirely), the developer is left with a silent, static video element.
This blank canvas forces the developer to reconstruct the fundamental mechanics of media playback. The play button is no longer a browser-given right; it is a JavaScript trigger calling the video.play() and video.pause() methods. This deconstruction highlights the elegance of the HTML5 Media API. The API provides a robust set of properties and methods—currentTime, duration, volume, and playbackRate—that allow developers to manipulate video behavior with granular precision. A CodePen project serves as the perfect sandbox to visualize this relationship, turning abstract JavaScript methods into tangible on-screen buttons.
2. Technical Implementation (The "How")
The backbone of these pens is the HTML5 Media API. The code structure is generally clean and follows a recognizable pattern:
- Wrappers: The
<video>element is wrapped in a container (e.g.,.video-container) to position the custom controls absolutely over the video. - Toggle Logic: JavaScript uses a simple event listener on the video (
click) to toggle.paused()or.play(). - Progress Bar Math: This is the most complex part. Developers calculate the click position on a progress bar (
e.offsetX / e.target.clientWidth) and map it to the video duration (video.currentTime).
The Good: It teaches the fundamentals of the Media API (play(), pause(), duration, currentTime, volume).
The Bad: Many pens rely heavily on jQuery or heavy libraries for simple state changes that vanilla JS handles effortlessly today.
Part 6: The Complete Codepen Workflow
To instantly deploy this, follow these steps:
- Go to CodePen.io and create a new Pen.
- HTML Panel: Paste the HTML structure from Part 1.
- CSS Panel: Paste the CSS from Part 2.
- JS Panel: Paste the JavaScript from Part 3 (plus speed controls if desired).
- Click Save and give it a title like "Custom HTML5 Video Player – Glassmorphism Design."
Pro Tip: In CodePen settings, ensure "Auto-Prefixer" is ON to handle vendor prefixes for the CSS backdrop filter.
Further Enhancements (Homework)
- Add a “loop” button.
- Implement volume boost (200%).
- Show buffering indicator.
- Add chapter bookmarks.
- Convert the entire player into a Web Component (
<custom-player>).
Ready to level up? Open CodePen, paste the code above, and start customizing. Your perfect video player is just a few keystrokes away.
Custom HTML5 video players on serve as functional prototypes for developers who need to move beyond the browser's default, unstylable video controls. Popular Custom Video Player Examples
CodePen hosts various implementations ranging from simple skins to complex, feature-rich players: JavaScript30 Custom Player
: A widely referenced project by Wes Bos that includes play/pause, volume sliders, playback rate controls, and skip buttons. HTML5 Video Player with Custom Controls
: A version using SCSS for styling and Intersection Observer for auto-playing videos when they enter the viewport. Interactive UI Skins
: Modern designs featuring picture-in-picture, airplay support, and custom-styled progress bars. Video with Chapters
: Advanced players that include interactive chapter markers and progress tracking. Core Functional Components
A standard custom player on CodePen typically consists of three layers: Getting Started with CodePen: A Beginner's Guide to CodePen
To create a custom HTML5 video player with a "solid paper" overlay (often used for play buttons, intros, or masking) in CodePen, follow this structure. You can reference similar implementations on for inspiration. 1. HTML Structure
and custom "paper" overlay in a container to manage positioning. Ensure the native controls are removed so your custom overlay can take over. "video-container" "video-element" "your-video-url.mp4" "paper-overlay" "play-btn" >Play Video "custom-controls" Use code with caution. Copied to clipboard 2. CSS for the "Paper" Effect
Use absolute positioning to make the overlay cover the video. To get a "solid paper" look, use a solid background color with subtle textures or shadows. ; overflow: hidden; }
.video-element width: ; width: ; height: ; background-color: #f4f1ea; /* "Paper" color / ; transition: opacity / Paper-like texture/shadows */ box-shadow: inset );
.paper-overlay.hidden opacity: ; pointer-events: none; Use code with caution. Copied to clipboard 3. JavaScript Logic custom html5 video player codepen
You need to handle the interaction where clicking the "paper" overlay triggers the video playback and hides the overlay. javascript container = document.querySelector( '.video-container' video = container.querySelector( '.video-element' overlay = container.querySelector( '.paper-overlay' playBtn = container.querySelector( '.play-btn' );
playBtn.addEventListener(
(video.paused) video.play(); overlay.classList.add( ); }); // Optional: Show overlay again when video ends video.addEventListener( , () => { overlay.classList.remove( Use code with caution. Copied to clipboard Implementation Tips Responsiveness width: 100% height: auto
on the video element to ensure it scales correctly across devices. Custom Controls
: If you want a fully custom UI, you can add event listeners for timeupdate to drive a custom progress bar.
: For advanced styling techniques like animated borders or complex UI, you can explore the JS30 Custom Video Player Vanilla JS Player examples on CodePen. custom control buttons like a progress bar or volume slider to this setup? HTML5 custom video player - CodePen
The Architecture of Progress: Logic and Styling
Perhaps the most intricate component of a custom video player is the progress bar. The default browser scrubber is functional but often difficult to style consistently across Chrome, Firefox, and Safari. In a custom implementation, the progress bar is usually constructed using a <div> container representing the total duration, with an inner child <div> representing the current progress.
The logic behind this requires coordinate geometry and event listening. Developers must calculate the ratio of the mouse click position relative to the total width of the progress bar and map that percentage to the video’s duration. Furthermore, a successful player—like those often featured on CodePen—includes a "buffer" indicator. By listening to the progress event and accessing the video's buffered property, developers can visually display how much of the video has pre-loaded. This transparency is a hallmark of good UX design, reassuring the user that the media is ready for consumption.
Styling these elements introduces the challenge of cross-browser compatibility. While the underlying logic is JavaScript, the visual polish is often handled via CSS Flexbox or Grid. Common CodePen examples utilize Font Awesome or SVG icons for the play/pause and volume buttons, allowing for scalable vector graphics that look crisp on high-DPI displays. This separation of concerns—using CSS for the "look" and JavaScript for the "state"—is a fundamental lesson for any aspiring front-end engineer.
Part 3: The JavaScript (The Engine)
This is the most critical section. We will use the HTML5 Media API to link the buttons to video functions.
// Get DOM elements const video = document.getElementById('myVideo'); const playPauseBtn = document.getElementById('playPauseBtn'); const progressBar = document.querySelector('.progress-bar'); const progressFill = document.getElementById('progressFill'); const timeDisplay = document.getElementById('timeDisplay'); const volumeSlider = document.getElementById('volumeSlider'); const fullscreenBtn = document.getElementById('fullscreenBtn');// 1. Play / Pause Logic function togglePlayPause() if (video.paused
playPauseBtn.addEventListener('click', togglePlayPause);
// 2. Update Progress Bar and Time as video plays video.addEventListener('timeupdate', () => const percentage = (video.currentTime / video.duration) * 100; progressFill.style.width =
$percentage%;// Format time display const currentMinutes = Math.floor(video.currentTime / 60); const currentSeconds = Math.floor(video.currentTime % 60); const durationMinutes = Math.floor(video.duration / 60); const durationSeconds = Math.floor(video.duration % 60);
timeDisplay.textContent =
$currentMinutes.toString().padStart(2, '0'):$currentSeconds.toString().padStart(2, '0') / $durationMinutes.toString().padStart(2, '0'):$durationSeconds.toString().padStart(2, '0'); );// 3. Seek Video when clicking on progress bar progressBar.addEventListener('click', (e) => const rect = progressBar.getBoundingClientRect(); const clickX = e.clientX - rect.left; const width = rect.width; const clickPercent = clickX / width; video.currentTime = clickPercent * video.duration; );
// 4. Volume Control volumeSlider.addEventListener('input', (e) => video.volume = e.target.value; ); Beyond the Native: The Art and Engineering of
// 5. Fullscreen functionality fullscreenBtn.addEventListener('click', () => const container = document.querySelector('.video-container'); if (!document.fullscreenElement) container.requestFullscreen(); else document.exitFullscreen(); );
// Optional: Auto-update play/pause button if video ends video.addEventListener('ended', () => playPauseBtn.textContent = '▶ Play'; );
JavaScript breakdown:
- Line 16-25: The
timeupdateevent fires every 250ms, updating the red progress bar and the timestamp. - Line 30-34: Clicking the progress bar calculates where the user clicked (using
clientXandgetBoundingClientRect) and jumps thecurrentTime. - Line 42-47:
requestFullscreenmakes the entire container go fullscreen, not just the video.
Summary Table
| Feature | Rating | Notes | | :--- | :--- | :--- | | Visual Design | ⭐⭐⭐⭐⭐ | Exceptional. Far superior to native browser styles. | | Code Quality | ⭐⭐⭐⭐ | Usually clean Vanilla JS/jQuery, easy to read. | | Functionality | ⭐⭐⭐ | Often missing advanced features (captions, playback speed). | | Accessibility | ⭐⭐ | Major failure point. Keyboard support is usually broken. | | Cross-Browser | ⭐⭐⭐ | Requires testing; Fullscreen behavior varies wildly. |
Final Recommendation
If you are looking to learn how the HTML5 Video API works, CodePen is the best place to start. Dissecting the math behind a progress bar is a fantastic exercise.
However, if you are looking for a solution to implement in a production website, do not copy-paste a CodePen snippet blindly. You are likely introducing accessibility lawsuits and maintenance headaches. Instead, use a battle-tested library like Plyr, Video.js, or Plyr. These libraries offer the beautiful UI of a CodePen demo but include the robust keyboard support, screen reader ARIA labels, and cross-browser stability that you need in the real world.
Creating a custom HTML5 video player allows you to match your site's branding and provide a unique user experience. By using the HTML5 Media API, you can replace browser-default controls with your own buttons, sliders, and progress bars. 🛠️ The Core Components Building a custom player requires three distinct layers:
HTML: Defines the video container and the control interface. CSS: Styles the layout, buttons, and responsive behavior.
JavaScript: Hooks into the video events (play, pause, volume) to update the UI. 🏗️ Step 1: Markup (HTML)
Wrap your tag and custom controls in a wrapper. This ensures you can hide the default controls and position your UI over the video.
Use code with caution. Copied to clipboard 🎨 Step 2: Styling (CSS)
Use CSS Flexbox or Grid to align your controls. Hide the native controls by omitting the controls attribute in HTML and use position: absolute to overlay your custom bar. Overlay: Put controls at the bottom of the container. Z-index: Ensure controls sit above the video layer.
Custom Sliders: Use input[type="range"] for progress and volume. ⚙️ Step 3: Logic (JavaScript)
This is where the magic happens. You need to listen for user clicks and video updates. Toggle Play: Use video.play() and video.pause(). Update Progress: Listen to the timeupdate event.
Scrubbing: Update video.currentTime when the progress slider moves. Volume: Map the volume slider value to video.volume. 🚀 Interactive Examples on CodePen
For live code and visual inspiration, check out these popular implementations: Clean & Minimal Player: Great for portfolio sites. Plyr.io Clone: A lightweight, accessible HTML5 player.
Netflix-style UI: Features custom overlays and big play icons. Wrappers: The <video> element is wrapped in a
📌 Pro Tip: Always include a "Mute" button. Autoplay videos often require the muted attribute to function in modern browsers like Chrome and Safari.
If you'd like, I can write the full source code (HTML, CSS, and JS) for a specific style, like a minimalist dark theme or a glassmorphism player. Which one would you prefer?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Custom HTML5 Video Player | Modern UI</title>
<style>
*
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none; /* avoid accidental selection on double-click */
body
background: linear-gradient(145deg, #1a1e2c 0%, #11141f 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', 'Poppins', system-ui, -apple-system, 'Inter', sans-serif;
padding: 20px;
/* MAIN PLAYER CARD */
.player-container
max-width: 1000px;
width: 100%;
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(2px);
border-radius: 32px;
box-shadow: 0 25px 45px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.08);
overflow: hidden;
transition: all 0.2s ease;
/* video wrapper (for custom controls overlay) */
.video-wrapper
position: relative;
background: #000;
width: 100%;
cursor: pointer;
video
width: 100%;
height: auto;
display: block;
vertical-align: middle;
/* ----- CUSTOM CONTROLS BAR (modern glass) ----- */
.custom-controls
background: rgba(20, 22, 36, 0.85);
backdrop-filter: blur(12px);
padding: 12px 18px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.15);
transition: opacity 0.25s ease;
font-size: 14px;
/* left group */
.controls-left
display: flex;
align-items: center;
gap: 14px;
flex: 2;
/* center group (progress) */
.controls-center
flex: 6;
min-width: 140px;
/* right group */
.controls-right
display: flex;
align-items: center;
gap: 18px;
flex: 2;
justify-content: flex-end;
/* buttons styling */
.ctrl-btn
background: transparent;
border: none;
color: #f0f0f0;
font-size: 20px;
width: 36px;
height: 36px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
backdrop-filter: blur(4px);
.ctrl-btn:hover
background: rgba(255, 255, 255, 0.2);
transform: scale(1.02);
.ctrl-btn:active
transform: scale(0.96);
/* time display */
.time-display
font-family: 'Monaco', 'Fira Mono', monospace;
font-size: 0.9rem;
background: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 40px;
letter-spacing: 0.5px;
color: #eef;
/* volume slider container */
.volume-wrap
display: flex;
align-items: center;
gap: 8px;
.volume-icon
font-size: 20px;
cursor: pointer;
background: none;
border: none;
color: #f0f0f0;
display: inline-flex;
align-items: center;
input[type="range"]
-webkit-appearance: none;
background: transparent;
cursor: pointer;
/* progress bar (seek) */
.progress-bar
flex: 1;
height: 5px;
background: rgba(255, 255, 255, 0.25);
border-radius: 20px;
position: relative;
cursor: pointer;
transition: height 0.1s;
.progress-bar:hover
height: 7px;
.progress-filled
width: 0%;
height: 100%;
background: linear-gradient(90deg, #e14eca, #d6409f, #ff7b89);
border-radius: 20px;
position: relative;
pointer-events: none;
.progress-filled::after
content: '';
position: absolute;
right: -6px;
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 12px;
background: #ffb3d9;
border-radius: 50%;
box-shadow: 0 0 6px #ff80b3;
opacity: 0;
transition: opacity 0.1s;
.progress-bar:hover .progress-filled::after
opacity: 1;
/* volume range style */
.volume-slider
width: 80px;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 5px;
input[type="range"]::-webkit-slider-thumb
-webkit-appearance: none;
width: 12px;
height: 12px;
background: white;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 2px #fff;
border: none;
/* speed dropdown */
.speed-select
background: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 6px 10px;
border-radius: 32px;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
outline: none;
backdrop-filter: blur(4px);
transition: 0.1s;
.speed-select:hover
background: rgba(30, 30, 50, 0.9);
/* fullscreen button */
.fullscreen-btn
font-size: 20px;
/* responsive adjustments */
@media (max-width: 680px)
.custom-controls
flex-wrap: wrap;
gap: 10px;
padding: 12px;
.controls-left, .controls-right
flex: auto;
.controls-center
order: 3;
flex: 1 1 100%;
margin-top: 6px;
.volume-slider
width: 60px;
.ctrl-btn
width: 32px;
height: 32px;
font-size: 18px;
.time-display
font-size: 0.75rem;
/* loading / error / poster style */
.video-wrapper .loading-indicator
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.7);
backdrop-filter: blur(6px);
padding: 10px 20px;
border-radius: 40px;
color: white;
font-size: 14px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
z-index: 10;
/* big play button overlay */
.big-play
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 70px;
height: 70px;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(10px);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 38px;
cursor: pointer;
transition: all 0.2s ease;
opacity: 0;
z-index: 15;
pointer-events: auto;
border: 1px solid rgba(255,255,255,0.3);
.big-play:hover
background: #e14eca;
transform: translate(-50%, -50%) scale(1.05);
color: white;
/* fade animations for controls hide/show */
.controls-hidden .custom-controls
opacity: 0;
visibility: hidden;
transition: visibility 0.2s, opacity 0.2s;
.video-wrapper:hover .custom-controls
opacity: 1;
visibility: visible;
/* default: visible, but on idle we hide via class toggled by js */
.custom-controls
visibility: visible;
transition: opacity 0.3s ease, visibility 0.3s;
/* mouse idle (no movement) - class added by js */
.idle-controls .custom-controls
opacity: 0;
visibility: hidden;
/* but on hover always show regardless of idle */
.video-wrapper:hover .custom-controls
opacity: 1 !important;
visibility: visible !important;
/* big play button also hides when playing */
.big-play.hide-big
display: none;
</style>
</head>
<body>
<div class="player-container">
<div class="video-wrapper" id="videoWrapper">
<video id="myVideo" poster="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg" preload="metadata">
<!-- sample video from sample-videos.com / big buck bunny (high quality) -->
<source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" type="video/mp4">
Your browser does not support HTML5 video.
</video>
<!-- big play button overlay -->
<div class="big-play" id="bigPlayBtn">▶</div>
<div class="loading-indicator" id="loadingIndicator">Loading...</div>
<!-- custom control bar -->
<div class="custom-controls" id="customControls">
<div class="controls-left">
<button class="ctrl-btn" id="playPauseBtn" aria-label="Play/Pause">⏸</button>
<div class="volume-wrap">
<button class="volume-icon" id="muteBtn" aria-label="Mute">🔊</button>
<input type="range" id="volumeSlider" class="volume-slider" min="0" max="1" step="0.01" value="1">
</div>
<div class="time-display">
<span id="currentTime">0:00</span> / <span id="duration">0:00</span>
</div>
</div>
<div class="controls-center">
<div class="progress-bar" id="progressBar">
<div class="progress-filled" id="progressFilled"></div>
</div>
</div>
<div class="controls-right">
<select id="speedSelect" class="speed-select">
<option value="0.5">0.5x</option>
<option value="0.75">0.75x</option>
<option value="1" selected>1x</option>
<option value="1.25">1.25x</option>
<option value="1.5">1.5x</option>
<option value="2">2x</option>
</select>
<button class="ctrl-btn fullscreen-btn" id="fullscreenBtn" aria-label="Fullscreen">⛶</button>
</div>
</div>
</div>
</div>
<script>
(function() {
// DOM elements
const video = document.getElementById('myVideo');
const wrapper = document.getElementById('videoWrapper');
const playPauseBtn = document.getElementById('playPauseBtn');
const bigPlayBtn = document.getElementById('bigPlayBtn');
const progressBar = document.getElementById('progressBar');
const progressFilled = document.getElementById('progressFilled');
const currentTimeSpan = document.getElementById('currentTime');
const durationSpan = document.getElementById('duration');
const volumeSlider = document.getElementById('volumeSlider');
const muteBtn = document.getElementById('muteBtn');
const speedSelect = document.getElementById('speedSelect');
const fullscreenBtn = document.getElementById('fullscreenBtn');
const loadingIndicator = document.getElementById('loadingIndicator');
// state
let controlsTimeout = null;
let isControlsIdle = false;
let isPlaying = false;
// Helper: format time (seconds to MM:SS)
function formatTime(seconds)
if (isNaN(seconds)) return "0:00";
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hrs > 0)
return `$hrs:$mins.toString().padStart(2, '0'):$secs.toString().padStart(2, '0')`;
return `$mins:$secs.toString().padStart(2, '0')`;
// update progress and time displays
function updateProgress()
if (video.duration && !isNaN(video.duration))
const percent = (video.currentTime / video.duration) * 100;
progressFilled.style.width = `$percent%`;
currentTimeSpan.innerText = formatTime(video.currentTime);
else
progressFilled.style.width = '0%';
currentTimeSpan.innerText = "0:00";
// update duration display
function updateDuration()
if (video.duration && !isNaN(video.duration))
durationSpan.innerText = formatTime(video.duration);
else
durationSpan.innerText = "0:00";
// play/pause toggles + big play button sync
function togglePlayPause() video.ended)
video.play();
updatePlayPauseUI(true);
hideBigPlayButton();
else
video.pause();
updatePlayPauseUI(false);
showBigPlayButtonIfNeeded();
function updatePlayPauseUI(playing)
isPlaying = playing;
if (playing)
playPauseBtn.innerHTML = "⏸";
playPauseBtn.setAttribute("aria-label", "Pause");
else
playPauseBtn.innerHTML = "▶";
playPauseBtn.setAttribute("aria-label", "Play");
function hideBigPlayButton()
bigPlayBtn.classList.add('hide-big');
function showBigPlayButtonIfNeeded()
if (video.paused && !video.ended)
bigPlayBtn.classList.remove('hide-big');
else
bigPlayBtn.classList.add('hide-big');
// seek using progress bar
function seek(e)
const rect = progressBar.getBoundingClientRect();
let clickX = e.clientX - rect.left;
let width = rect.width;
if (width > 0 && video.duration)
const percent = Math.min(Math.max(clickX / width, 0), 1);
video.currentTime = percent * video.duration;
updateProgress();
// volume
function updateVolume()
video.volume = volumeSlider.value;
if (video.volume === 0)
muteBtn.innerHTML = "🔇";
else if (video.volume < 0.5)
muteBtn.innerHTML = "🔉";
else
muteBtn.innerHTML = "🔊";
function toggleMute()
if (video.volume === 0)
video.volume = volumeSlider.value = 0.5;
else
video.volume = 0;
volumeSlider.value = 0;
updateVolume();
// speed change
function changeSpeed()
video.playbackRate = parseFloat(speedSelect.value);
// fullscreen (modern api)
function toggleFullscreen()
const elem = wrapper;
if (!document.fullscreenElement)
if (elem.requestFullscreen)
elem.requestFullscreen().catch(err =>
console.warn(`Fullscreen error: $err.message`);
);
else if (elem.webkitRequestFullscreen)
elem.webkitRequestFullscreen();
else if (elem.msRequestFullscreen)
elem.msRequestFullscreen();
else
document.exitFullscreen();
// idle controls (hide after mouse inactivity)
function resetControlsIdleTimer()
if (controlsTimeout) clearTimeout(controlsTimeout);
if (wrapper.classList.contains('idle-controls'))
wrapper.classList.remove('idle-controls');
controlsTimeout = setTimeout(() =>
// only if video is playing and mouse not over wrapper (but we also will check hover)
// we add idle class only if playing, else keep controls visible.
if (!video.paused && !video.ended)
wrapper.classList.add('idle-controls');
else
// if paused, we do not hide controls
wrapper.classList.remove('idle-controls');
, 2000);
// event listeners for idle management
function initIdleHandling()
wrapper.addEventListener('mousemove', resetControlsIdleTimer);
wrapper.addEventListener('mouseleave', () =>
if (controlsTimeout) clearTimeout(controlsTimeout);
if (!video.paused && !video.ended)
wrapper.classList.add('idle-controls');
else
wrapper.classList.remove('idle-controls');
);
wrapper.addEventListener('mouseenter', () =>
wrapper.classList.remove('idle-controls');
resetControlsIdleTimer();
);
resetControlsIdleTimer();
// loading spinner handling
function handleLoadingStart()
loadingIndicator.style.opacity = '1';
function handleCanPlay()
loadingIndicator.style.opacity = '0';
updateDuration();
updateProgress();
function handleWaiting()
loadingIndicator.style.opacity = '1';
function handlePlaying()
loadingIndicator.style.opacity = '0';
// big play button handler
function onBigPlayClick()
togglePlayPause();
// keyboard shortcuts (space, k, f)
function handleKeyPress(e)
// when video ends
function onVideoEnded()
updatePlayPauseUI(false);
showBigPlayButtonIfNeeded();
wrapper.classList.remove('idle-controls'); // show controls when ended
if (controlsTimeout) clearTimeout(controlsTimeout);
// when video starts playing
function onVideoPlay()
updatePlayPauseUI(true);
hideBigPlayButton();
resetControlsIdleTimer();
function onVideoPause()
updatePlayPauseUI(false);
showBigPlayButtonIfNeeded();
wrapper.classList.remove('idle-controls'); // force controls visible on pause
if (controlsTimeout) clearTimeout(controlsTimeout);
// event binding
video.addEventListener('loadedmetadata', () =>
updateDuration();
updateProgress();
);
video.addEventListener('timeupdate', updateProgress);
video.addEventListener('play', onVideoPlay);
video.addEventListener('playing', () => loadingIndicator.style.opacity = '0'; );
video.addEventListener('pause', onVideoPause);
video.addEventListener('ended', onVideoEnded);
video.addEventListener('waiting', handleWaiting);
video.addEventListener('canplay', handleCanPlay);
video.addEventListener('loadstart', handleLoadingStart);
playPauseBtn.addEventListener('click', togglePlayPause);
bigPlayBtn.addEventListener('click', onBigPlayClick);
progressBar.addEventListener('click', seek);
volumeSlider.addEventListener('input', () =>
video.volume = volumeSlider.value;
updateVolume();
);
muteBtn.addEventListener('click', toggleMute);
speedSelect.addEventListener('change', changeSpeed);
fullscreenBtn.addEventListener('click', toggleFullscreen);
// additional double click on video toggles fullscreen?
video.addEventListener('dblclick', () =>
toggleFullscreen();
);
// click on video toggles play/pause (optional UX)
video.addEventListener('click', (e) =>
e.stopPropagation();
togglePlayPause();
);
// handle volume init
updateVolume();
// set initial play button icon because video is initially paused (showing poster)
updatePlayPauseUI(false);
// show big play button initially because video is paused
bigPlayBtn.classList.remove('hide-big');
// if video is already loaded (cached) ensure duration shown
if (video.readyState >= 1)
updateDuration();
updateProgress();
// Fix potential Firefox/Edge issues: set default speed
video.playbackRate = 1;
// idle controls handler init
initIdleHandling();
// prevent context menu on video for cleaner UX (optional)
video.addEventListener('contextmenu', (e) => e.preventDefault());
// Additional small improvement: when seeking via progress bar show time
progressBar.addEventListener('mousemove', (e) =>
// optional tooltip preview (nice to have but not mandatory)
);
// ensure that if video duration changes (livestream not needed)
window.addEventListener('resize', () => {});
console.log('Custom video player ready!');
})();
</script>
</body>
</html>
Essential Parts HTML5 tag: The engine. CSS3 Styling: The skin. JavaScript API: The brain. Simple Code Structure
Use code with caution. Copied to clipboard CSS (Key Styles) Flexbox: Align controls easily. Relative Positioning: Keep controls on top. Transition: Smooth hover effects. JavaScript (Core Logic) javascript
const video = document.querySelector('.viewer'); const toggle = document.querySelector('.toggle'); function togglePlay() const method = video.paused ? 'play' : 'pause'; video[method](); video.addEventListener('click', togglePlay); toggle.addEventListener('click', togglePlay); Use code with caution. Copied to clipboard Popular Features to Add Custom Progress Bar: Click-and-drag seeking. Playback Speed: Toggle from 0.5x to 2x. Skip Buttons: Quick ±10 second jumps. Full-Screen: Use the .requestFullscreen() API. Pro-Tips for CodePen Use Placeholder Videos: Link to Pexels for free hosting. Icon Fonts: Use FontAwesome for play/pause icons. Mobile-First: Ensure buttons are touch-friendly.
📌 Key Takeaway: Focus on the video object's properties like .paused, .currentTime, and .volume.
Introduction
HTML5 video players have become a crucial component of modern web development, allowing users to play video content directly in the browser. While default video players provided by browsers are functional, custom HTML5 video players offer a more tailored and engaging user experience. In this report, we'll explore the concept of custom HTML5 video players and highlight a notable example on CodePen.
What is a Custom HTML5 Video Player?
A custom HTML5 video player is a player that uses HTML5, CSS3, and JavaScript to provide a unique and interactive video playback experience. Unlike the default video players provided by browsers, custom players can be designed to match a website's branding, offer advanced controls, and provide a more engaging user experience.
Benefits of Custom HTML5 Video Players
- Improved User Experience: Custom video players can be designed to be more intuitive and engaging, providing a better experience for users.
- Branding and Customization: Custom players can be tailored to match a website's branding, ensuring a consistent visual identity.
- Advanced Controls: Custom players can offer advanced controls, such as playlists, closed captions, and social sharing buttons.
- Cross-Browser Compatibility: Custom players can be designed to work across multiple browsers and devices.
Example: Custom HTML5 Video Player on CodePen
One notable example of a custom HTML5 video player is the "Custom HTML5 Video Player" by @CodePen on CodePen. This example showcases a simple yet feature-rich video player that includes:
- Customizable design: The player features a clean and minimalistic design that can be easily customized to match a website's branding.
- Play/pause, seek, and volume controls: The player includes basic controls for play/pause, seek, and volume adjustment.
- Fullscreen support: The player supports fullscreen mode, allowing users to enjoy video content on larger screens.
CodePen Example Code
The CodePen example uses the following HTML, CSS, and JavaScript code:
HTML:
<div class="video-player">
<video id="video" src="https://example.com/video.mp4" poster="https://example.com/poster.jpg"></video>
<div class="controls">
<button class="play-pause">Play/Pause</button>
<input type="range" id="seek" min="0" max="100" value="0">
<button class="fullscreen">Fullscreen</button>
</div>
</div>
CSS (using SCSS):
.video-player
position: relative;
width: 640px;
height: 360px;
// ...
.video-player .controls
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 10px;
background-color: rgba(0, 0, 0, 0.5);
// ...
JavaScript:
const video = document.getElementById('video');
const seek = document.getElementById('seek');
const playPauseButton = document.querySelector('.play-pause');
const fullscreenButton = document.querySelector('.fullscreen');
// Add event listeners
playPauseButton.addEventListener('click', () =>
if (video.paused)
video.play();
else
video.pause();
);
seek.addEventListener('input', () =>
video.currentTime = (seek.value / 100) * video.duration;
);
// ...
Conclusion
Custom HTML5 video players offer a powerful way to enhance the user experience and provide a more engaging video playback experience. The CodePen example showcased in this report demonstrates a simple yet feature-rich custom video player that can be easily customized and integrated into a website. By using HTML5, CSS3, and JavaScript, developers can create custom video players that meet their specific needs and provide a more enjoyable experience for users.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Custom HTML5 Video Player | Modern UI | CodePen Ready</title>
<style>
*
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none; /* prevents accidental selection on double clicks */
body
background: linear-gradient(145deg, #0b1120 0%, #111827 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', 'Inter', system-ui, -apple-system, 'Roboto', sans-serif;
padding: 20px;
/* MAIN PLAYER CARD */
.player-container
max-width: 1000px;
width: 100%;
background: rgba(15, 25, 45, 0.65);
backdrop-filter: blur(8px);
border-radius: 2rem;
box-shadow: 0 25px 45px -12px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.08);
padding: 1rem;
transition: all 0.2s ease;
/* VIDEO WRAPPER (for aspect ratio & rounded corners) */
.video-wrapper
position: relative;
width: 100%;
border-radius: 1.25rem;
overflow: hidden;
background: #000;
box-shadow: 0 12px 28px -8px rgba(0, 0, 0, 0.5);
video
width: 100%;
height: auto;
display: block;
vertical-align: middle;
cursor: pointer;
/* CUSTOM CONTROLS BAR */
.custom-controls
background: rgba(10, 15, 25, 0.85);
backdrop-filter: blur(12px);
border-radius: 2rem;
margin-top: 1rem;
padding: 0.6rem 1.2rem;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: 0.2s;
/* BUTTON STYLES */
.ctrl-btn
background: transparent;
border: none;
color: #f0f3fa;
font-size: 1.4rem;
width: 38px;
height: 38px;
border-radius: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.2, 0.9, 0.4, 1.1);
backdrop-filter: blur(4px);
.ctrl-btn:hover
background: rgba(255, 255, 255, 0.2);
transform: scale(1.05);
.ctrl-btn:active
transform: scale(0.96);
/* PROGRESS BAR AREA */
.progress-area
flex: 3;
min-width: 140px;
display: flex;
align-items: center;
gap: 0.6rem;
.time-display
font-size: 0.85rem;
font-family: monospace;
letter-spacing: 0.5px;
background: rgba(0, 0, 0, 0.5);
padding: 0.2rem 0.6rem;
border-radius: 30px;
color: #e2e8ff;
font-weight: 500;
.progress-bar-bg
flex: 1;
height: 5px;
background: rgba(255, 255, 255, 0.25);
border-radius: 8px;
cursor: pointer;
position: relative;
transition: height 0.1s;
.progress-bar-bg:hover
height: 7px;
.progress-fill
width: 0%;
height: 100%;
background: linear-gradient(90deg, #f97316, #f59e0b);
border-radius: 8px;
position: relative;
pointer-events: none;
/* VOLUME CONTROL */
.volume-control
display: flex;
align-items: center;
gap: 0.5rem;
background: rgba(0, 0, 0, 0.4);
padding: 0 0.5rem;
border-radius: 40px;
.volume-slider
width: 85px;
height: 4px;
-webkit-appearance: none;
background: rgba(255, 255, 255, 0.3);
border-radius: 5px;
outline: none;
cursor: pointer;
.volume-slider::-webkit-slider-thumb
-webkit-appearance: none;
width: 12px;
height: 12px;
background: #f97316;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 4px white;
border: none;
/* SPEED DROPDOWN */
.speed-select
background: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
padding: 0.4rem 0.7rem;
border-radius: 2rem;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
outline: none;
transition: 0.1s;
font-family: inherit;
.speed-select option
background: #1e293b;
/* fullscreen button */
.fullscreen-btn
font-size: 1.3rem;
/* responsive */
@media (max-width: 650px)
.custom-controls
flex-wrap: wrap;
padding: 0.8rem;
gap: 0.5rem;
.progress-area
order: 1;
width: 100%;
flex-basis: 100%;
margin-top: 0.2rem;
.volume-control
order: 2;
.ctrl-btn, .speed-select
order: 3;
/* tooltip simulation */
.ctrl-btn[title]
position: relative;
/* loading / error / info (none active by default) */
.player-message
position: absolute;
bottom: 20px;
right: 20px;
background: #000000aa;
backdrop-filter: blur(8px);
padding: 0.3rem 1rem;
border-radius: 30px;
font-size: 0.75rem;
color: #ddd;
pointer-events: none;
font-family: monospace;
z-index: 5;
</style>
</head>
<body>
<div class="player-container">
<div class="video-wrapper" id="videoWrapper">
<video id="customVideo" preload="metadata" poster="https://assets.codepen.io/9827620/sample-poster.jpg?text=Custom+Player+Demo">
<!-- Sample video source (Big Buck Bunny short segment - royalty friendly from samples) -->
<source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4" type="video/mp4">
Your browser does not support HTML5 video.
</video>
<div class="player-message" id="statusMsg">▶ Ready</div>
</div>
<div class="custom-controls">
<!-- Play / Pause -->
<button class="ctrl-btn" id="playPauseBtn" title="Play/Pause (k)">
<span id="playIcon">▶</span>
</button>
<!-- Stop button (reset to beginning & pause) -->
<button class="ctrl-btn" id="stopBtn" title="Stop">⏹</button>
<!-- Progress & time -->
<div class="progress-area">
<span class="time-display" id="currentTimeUI">0:00</span>
<div class="progress-bar-bg" id="progressBarBg">
<div class="progress-fill" id="progressFill"></div>
</div>
<span class="time-display" id="durationUI">0:00</span>
</div>
<!-- Volume control -->
<div class="volume-control">
<button class="ctrl-btn" id="volumeBtn" title="Mute / Unmute">🔊</button>
<input type="range" id="volumeSlider" class="volume-slider" min="0" max="1" step="0.01" value="0.7">
</div>
<!-- Playback Speed -->
<select id="playbackSpeed" class="speed-select" title="Playback Speed">
<option value="0.5">0.5x</option>
<option value="0.75">0.75x</option>
<option value="1" selected>1x</option>
<option value="1.25">1.25x</option>
<option value="1.5">1.5x</option>
<option value="2">2x</option>
</select>
<!-- Fullscreen Toggle -->
<button class="ctrl-btn fullscreen-btn" id="fullscreenBtn" title="Fullscreen (f)">⛶</button>
</div>
</div>
<script>
(function()
// DOM Elements
const video = document.getElementById('customVideo');
const playPauseBtn = document.getElementById('playPauseBtn');
const playIconSpan = document.getElementById('playIcon');
const stopBtn = document.getElementById('stopBtn');
const progressBg = document.getElementById('progressBarBg');
const progressFill = document.getElementById('progressFill');
const currentTimeSpan = document.getElementById('currentTimeUI');
const durationSpan = document.getElementById('durationUI');
const volumeSlider = document.getElementById('volumeSlider');
const volumeBtn = document.getElementById('volumeBtn');
const speedSelect = document.getElementById('playbackSpeed');
const fullscreenBtn = document.getElementById('fullscreenBtn');
const videoWrapper = document.getElementById('videoWrapper');
const statusMsg = document.getElementById('statusMsg');
// Helper: format time (seconds) -> MM:SS or HH:MM:SS if needed but simple mm:ss
function formatTime(seconds)
if (isNaN(seconds)
// Update progress bar and time displays
function updateProgress()
// Set duration display
function setDurationDisplay()
if (video.duration && isFinite(video.duration))
durationSpan.textContent = formatTime(video.duration);
else
durationSpan.textContent = "0:00";
// Play/Pause toggle
function togglePlayPause()
if (video.paused)
video.play().catch(e =>
console.warn("Playback error:", e);
statusMsg.textContent = "⚠️ Playback blocked?";
setTimeout(() => if(statusMsg.textContent.includes("blocked")) statusMsg.textContent = "▶ Ready"; , 2000);
);
playIconSpan.textContent = "⏸";
statusMsg.textContent = "▶ Playing";
setTimeout(() => if(statusMsg.textContent === "▶ Playing") statusMsg.textContent = "🎬 Live"; , 1200);
else
video.pause();
playIconSpan.textContent = "▶";
statusMsg.textContent = "⏸ Paused";
setTimeout(() => if(statusMsg.textContent === "⏸ Paused") statusMsg.textContent = "▶ Ready"; , 1000);
// Stop: reset to beginning and pause
function stopVideo()
video.pause();
video.currentTime = 0;
playIconSpan.textContent = "▶";
updateProgress();
statusMsg.textContent = "⏹ Stopped";
setTimeout(() => if(statusMsg.textContent === "⏹ Stopped") statusMsg.textContent = "▶ Ready"; , 1000);
// Seek via progress bar click
function seek(event)
const rect = progressBg.getBoundingClientRect();
const clickX = event.clientX - rect.left;
const width = rect.width;
if (width > 0 && video.duration)
const seekTime = (clickX / width) * video.duration;
video.currentTime = seekTime;
updateProgress();
// Volume update
function setVolume(value)
let vol = parseFloat(value);
if (isNaN(vol)) vol = 0.7;
video.volume = vol;
volumeSlider.value = vol;
// update mute button icon
if (vol === 0)
volumeBtn.textContent = "🔇";
else if (vol < 0.3)
volumeBtn.textContent = "🔈";
else
volumeBtn.textContent = "🔊";
function toggleMute()
if (video.muted)
video.muted = false;
setVolume(video.volume);
statusMsg.textContent = "🔊 Unmuted";
else
video.muted = true;
volumeBtn.textContent = "🔇";
statusMsg.textContent = "🔇 Muted";
setTimeout(() => if(statusMsg.textContent === "🔇 Muted" , 800);
// Sync volume slider & button after mute/unmute externally or volume changes
function syncVolumeUI()
if (video.muted)
volumeBtn.textContent = "🔇";
volumeSlider.value = 0;
else
volumeSlider.value = video.volume;
if (video.volume === 0) volumeBtn.textContent = "🔇";
else if (video.volume < 0.3) volumeBtn.textContent = "🔈";
else volumeBtn.textContent = "🔊";
// Speed change
function changePlaybackSpeed()
video.playbackRate = parseFloat(speedSelect.value);
statusMsg.textContent = `⚡ $video.playbackRatex`;
setTimeout(() => if(statusMsg.textContent.includes("⚡")) statusMsg.textContent = "▶ Ready"; , 800);
// Fullscreen handling (with cross-browser)
function toggleFullscreen()
const container = videoWrapper;
if (!document.fullscreenElement && !document.webkitFullscreenElement) container.msRequestFullscreen;
if (requestMethod)
requestMethod.call(container).catch(err =>
statusMsg.textContent = "⚠️ Fullscreen not allowed";
setTimeout(() => if(statusMsg.textContent.includes("not allowed")) statusMsg.textContent = "▶ Ready"; , 1500);
);
else document.webkitExitFullscreen;
if (exitMethod) exitMethod.call(document);
// Listen to fullscreen change to adjust potential styling (optional)
function onFullscreenChange()
if (!document.fullscreenElement && !document.webkitFullscreenElement)
// optional UI hint
document.addEventListener('fullscreenchange', onFullscreenChange);
document.addEventListener('webkitfullscreenchange', onFullscreenChange);
// ---- VIDEO EVENT HANDLERS ----
video.addEventListener('loadedmetadata', () =>
setDurationDisplay();
updateProgress();
if (video.readyState >= 1)
durationSpan.textContent = formatTime(video.duration);
);
video.addEventListener('timeupdate', updateProgress);
video.addEventListener('play', () =>
playIconSpan.textContent = "⏸";
);
video.addEventListener('pause', () =>
playIconSpan.textContent = "▶";
);
video.addEventListener('volumechange', () =>
syncVolumeUI();
);
video.addEventListener('ended', () =>
playIconSpan.textContent = "▶";
statusMsg.textContent = "🏁 Ended";
setTimeout(() => if(statusMsg.textContent === "🏁 Ended") statusMsg.textContent = "▶ Ready"; , 1500);
updateProgress();
);
video.addEventListener('waiting', () =>
statusMsg.textContent = "⏳ Buffering...";
);
video.addEventListener('canplay', () =>
if(statusMsg.textContent === "⏳ Buffering...") statusMsg.textContent = "▶ Ready";
setDurationDisplay();
);
// initial volume set
video.volume = 0.7;
video.muted = false;
syncVolumeUI();
// Load start: ensure duration and stuff
if (video.readyState >= 1)
setDurationDisplay();
else
video.addEventListener('loadeddata', setDurationDisplay);
// ----- EVENT LISTENERS -----
playPauseBtn.addEventListener('click', togglePlayPause);
stopBtn.addEventListener('click', stopVideo);
progressBg.addEventListener('click', seek);
volumeSlider.addEventListener('input', (e) =>
if (video.muted) video.muted = false;
setVolume(e.target.value);
);
volumeBtn.addEventListener('click', toggleMute);
speedSelect.addEventListener('change', changePlaybackSpeed);
fullscreenBtn.addEventListener('click', toggleFullscreen);
// Keyboard shortcuts (nice extra feature)
window.addEventListener('keydown', (e) => tag === 'SELECT' );
// For double-click on video to toggle fullscreen (optional)
video.addEventListener('dblclick', () =>
toggleFullscreen();
);
// click on video toggles play/pause
video.addEventListener('click', () =>
togglePlayPause();
);
// small tooltip: display current volume or speed on slider hover
volumeSlider.addEventListener('mouseenter', () =>
statusMsg.textContent = `Volume: $Math.round(video.volume * 100)%`;
);
volumeSlider.addEventListener('mouseleave', () =>
if(!statusMsg.textContent.includes("Volume") && !statusMsg.textContent.includes("x") && !statusMsg.textContent.includes("s"))
statusMsg.textContent = "▶ Ready";
else if(statusMsg.textContent.includes("Volume")) statusMsg.textContent = "▶ Ready";
);
// Ensure progress fill reflects initial state
setDurationDisplay();
updateProgress();
// Edge case: if video src fails, show fallback message
video.addEventListener('error', (e) =>
console.error("Video error", e);
statusMsg.textContent = "⚠️ Video source error";
);
// Demo info: show that custom player is active
console.log("Custom HTML5 Video Player Loaded )();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Custom HTML5 Video Player | Sleek Design</title>
<style>
/* ------------------------------
GLOBAL RESET & BASE STYLES
-------------------------------- */
*
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none; /* avoid accidental text selection on UI */
body
background: linear-gradient(145deg, #0b1a2e 0%, #0a111f 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
padding: 1.5rem;
/* main card container */
.player-container
max-width: 1100px;
width: 100%;
background: rgba(10, 20, 30, 0.65);
backdrop-filter: blur(4px);
border-radius: 2rem;
padding: 1.2rem;
box-shadow: 0 25px 45px -12px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.05);
/* ----- CUSTOM VIDEO WRAPPER ----- */
.video-wrapper
position: relative;
width: 100%;
border-radius: 1.2rem;
overflow: hidden;
background: #000;
box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.5);
transition: box-shadow 0.2s ease;
/* native video element */
#videoPlayer
width: 100%;
height: auto;
display: block;
cursor: pointer;
aspect-ratio: 16 / 9;
object-fit: contain;
background: #000;
/* ----- CUSTOM CONTROLS BAR ----- */
.custom-controls
background: rgba(20, 28, 38, 0.92);
backdrop-filter: blur(12px);
border-radius: 2rem;
margin-top: 1rem;
padding: 0.7rem 1.2rem;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.8rem;
transition: all 0.2s;
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
/* button styling */
.ctrl-btn
background: transparent;
border: none;
color: #eef4ff;
font-size: 1.3rem;
width: 40px;
height: 40px;
border-radius: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(2px);
.ctrl-btn:hover
background: rgba(255, 255, 255, 0.2);
transform: scale(1.05);
color: white;
.ctrl-btn:active
transform: scale(0.96);
/* progress bar container */
.progress-container
flex: 3;
min-width: 140px;
display: flex;
align-items: center;
gap: 0.7rem;
.progress-bar-bg
flex: 1;
height: 5px;
background: rgba(255, 255, 255, 0.25);
border-radius: 20px;
cursor: pointer;
position: relative;
transition: height 0.1s;
.progress-bar-bg:hover
height: 7px;
.progress-fill
width: 0%;
height: 100%;
background: linear-gradient(90deg, #f97316, #ffb347);
border-radius: 20px;
position: relative;
pointer-events: none;
/* time display */
.time-display
font-size: 0.85rem;
font-weight: 500;
background: rgba(0, 0, 0, 0.5);
padding: 0.25rem 0.7rem;
border-radius: 30px;
letter-spacing: 0.3px;
font-family: 'Monaco', 'Cascadia Code', monospace;
color: #ddd;
/* volume section */
.volume-container
display: flex;
align-items: center;
gap: 0.5rem;
background: rgba(0, 0, 0, 0.3);
padding: 0.2rem 0.8rem;
border-radius: 40px;
.volume-slider
width: 80px;
cursor: pointer;
background: #2c3e44;
height: 4px;
border-radius: 4px;
-webkit-appearance: none;
.volume-slider:focus
outline: none;
.volume-slider::-webkit-slider-thumb
-webkit-appearance: none;
width: 12px;
height: 12px;
background: #ffb347;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 2px white;
/* speed dropdown */
.speed-select
background: rgba(0, 0, 0, 0.65);
border: 1px solid rgba(255, 166, 70, 0.5);
border-radius: 28px;
color: white;
padding: 0.35rem 0.7rem;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
font-family: inherit;
transition: 0.1s;
.speed-select:hover
background: #f97316cc;
border-color: #ffd966;
/* fullscreen button */
.fullscreen-btn
font-size: 1.3rem;
/* responsive adjustments */
@media (max-width: 680px)
.custom-controls
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem;
padding: 0.8rem;
.progress-container
order: 1;
width: 100%;
flex-basis: 100%;
margin: 0.2rem 0;
.volume-container
margin-left: auto;
.ctrl-btn
width: 36px;
height: 36px;
font-size: 1.1rem;
.time-display
font-size: 0.7rem;
/* loading / buffering indicator */
.loading-indicator
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 48px;
height: 48px;
border-radius: 50%;
background: rgba(0,0,0,0.65);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
z-index: 10;
.spinner
width: 30px;
height: 30px;
border: 3px solid rgba(255,165,70,0.3);
border-top: 3px solid #ffb347;
border-radius: 50%;
animation: spin 0.8s linear infinite;
@keyframes spin
to transform: rotate(360deg);
/* big play overlay (optional) */
.big-play
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s;
z-index: 5;
.big-play-icon
font-size: 4.5rem;
color: white;
text-shadow: 0 2px 12px black;
background: rgba(0,0,0,0.5);
width: 90px;
height: 90px;
border-radius: 100px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10px);
transition: transform 0.1s;
.video-wrapper:hover .big-play
opacity: 0.6;
</style>
</head>
<body>
<div class="player-container">
<div class="video-wrapper" id="videoWrapper">
<video id="videoPlayer" preload="metadata" poster="https://assets.codepen.io/9827620/sample-poster.jpg" crossorigin="anonymous">
<!-- Sample video source (Big Buck Bunny short, royalty-free and widely available) -->
<source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4" type="video/mp4">
Your browser does not support HTML5 video.
</video>
<!-- loading spinner -->
<div class="loading-indicator" id="loadingSpinner">
<div class="spinner"></div>
</div>
<!-- big play overlay (visual only) -->
<div class="big-play" id="bigPlayOverlay">
<div class="big-play-icon">▶</div>
</div>
</div>
<!-- Custom Control Bar -->
<div class="custom-controls">
<!-- play/pause -->
<button class="ctrl-btn" id="playPauseBtn" aria-label="Play/Pause">⏸</button>
<!-- progress & time -->
<div class="progress-container">
<div class="progress-bar-bg" id="progressBar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="time-display" id="timeDisplay">0:00 / 0:00</div>
</div>
<!-- volume control -->
<div class="volume-container">
<button class="ctrl-btn" id="volumeBtn" aria-label="Mute/Unmute">🔊</button>
<input type="range" id="volumeSlider" class="volume-slider" min="0" max="1" step="0.02" value="0.8">
</div>
<!-- playback speed -->
<select id="speedSelect" class="speed-select">
<option value="0.5">0.5x</option>
<option value="0.75">0.75x</option>
<option value="1" selected>1x</option>
<option value="1.25">1.25x</option>
<option value="1.5">1.5x</option>
<option value="2">2x</option>
</select>
<!-- fullscreen button -->
<button class="ctrl-btn fullscreen-btn" id="fullscreenBtn" aria-label="Fullscreen">⤢</button>
</div>
</div>
<script>
(function()
// ----- DOM elements -----
const video = document.getElementById('videoPlayer');
const playPauseBtn = document.getElementById('playPauseBtn');
const progressFill = document.getElementById('progressFill');
const progressBarBg = document.getElementById('progressBar');
const timeDisplay = document.getElementById('timeDisplay');
const volumeBtn = document.getElementById('volumeBtn');
const volumeSlider = document.getElementById('volumeSlider');
const speedSelect = document.getElementById('speedSelect');
const fullscreenBtn = document.getElementById('fullscreenBtn');
const loadingSpinner = document.getElementById('loadingSpinner');
const bigPlayOverlay = document.getElementById('bigPlayOverlay');
const videoWrapper = document.getElementById('videoWrapper');
// ----- state flags -----
let isDraggingProgress = false;
let controlsTimeout = null;
let isControlsVisible = true;
// Helper: format time (seconds -> MM:SS or HH:MM:SS? but typical video length)
function formatTime(seconds)
if (isNaN(seconds)) return "0:00";
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hrs > 0)
return `$hrs:$mins.toString().padStart(2, '0'):$secs.toString().padStart(2, '0')`;
return `$mins:$secs.toString().padStart(2, '0')`;
// update progress bar and time display
function updateProgress()
if (!isDraggingProgress)
// update time display
const current = formatTime(video.currentTime);
const total = formatTime(video.duration);
timeDisplay.textContent = `$current / $total`;
// set video progress based on click/drag on progress bar
function seekTo(event)
const rect = progressBarBg.getBoundingClientRect();
let clickX = event.clientX - rect.left;
let width = rect.width;
if (clickX < 0) clickX = 0;
if (clickX > width) clickX = width;
const percent = clickX / width;
if (video.duration)
video.currentTime = percent * video.duration;
updateProgress();
// ---- Play/Pause logic & UI icon ----
function updatePlayPauseIcon()
if (video.paused)
playPauseBtn.innerHTML = '▶';
playPauseBtn.setAttribute('aria-label', 'Play');
else
playPauseBtn.innerHTML = '⏸';
playPauseBtn.setAttribute('aria-label', 'Pause');
function togglePlayPause()
if (video.paused)
video.play().catch(e => console.warn("Playback prevented:", e));
else
video.pause();
updatePlayPauseIcon();
// ---- Volume & mute ----
function updateVolumeIcon()
function setVolume(value)
let vol = parseFloat(value);
if (isNaN(vol)) vol = 0.8;
vol = Math.min(1, Math.max(0, vol));
video.volume = vol;
video.muted = (vol === 0);
volumeSlider.value = vol;
updateVolumeIcon();
function toggleMute()
if (video.muted)
video.muted = false;
if (video.volume === 0) setVolume(0.6);
else
video.muted = true;
updateVolumeIcon();
volumeSlider.value = video.muted ? 0 : video.volume;
// ---- Speed ----
function updatePlaybackSpeed()
video.playbackRate = parseFloat(speedSelect.value);
// ---- FULLSCREEN API (cross-browser) ----
function toggleFullscreen()
const elem = videoWrapper;
if (!document.fullscreenElement)
if (elem.requestFullscreen)
elem.requestFullscreen().catch(err =>
console.warn(`Fullscreen error: $err.message`);
);
else if (elem.webkitRequestFullscreen) /* Safari */
elem.webkitRequestFullscreen();
else if (elem.msRequestFullscreen)
elem.msRequestFullscreen();
else
if (document.exitFullscreen)
document.exitFullscreen();
else if (document.webkitExitFullscreen)
document.webkitExitFullscreen();
// ---- loading spinner handling ----
function showLoading(show)
if (show)
loadingSpinner.style.opacity = '1';
else
loadingSpinner.style.opacity = '0';
// ---- big play overlay click handler (optional, same as video click) ----
function handleVideoClick()
togglePlayPause();
// ---- hide/show auto-hide for controls (extra polish) ----
function resetControlsTimeout()
if (controlsTimeout) clearTimeout(controlsTimeout);
const controlsBar = document.querySelector('.custom-controls');
if (!video.paused)
controlsBar.style.opacity = '1';
controlsBar.style.transform = 'translateY(0)';
isControlsVisible = true;
controlsTimeout = setTimeout(() =>
if (!video.paused && !isDraggingProgress)
controlsBar.style.opacity = '0';
controlsBar.style.transform = 'translateY(12px)';
isControlsVisible = false;
, 2500);
else
// when paused, keep controls visible
controlsBar.style.opacity = '1';
controlsBar.style.transform = 'translateY(0)';
if (controlsTimeout) clearTimeout(controlsTimeout);
function showControlsTemporarily()
const controlsBar = document.querySelector('.custom-controls');
controlsBar.style.opacity = '1';
controlsBar.style.transform = 'translateY(0)';
if (controlsTimeout) clearTimeout(controlsTimeout);
if (!video.paused)
controlsTimeout = setTimeout(() =>
if (!video.paused && !isDraggingProgress)
controlsBar.style.opacity = '0';
controlsBar.style.transform = 'translateY(12px)';
, 2500);
// ---- event listeners ----
function initEventListeners()
// video events
video.addEventListener('play', () =>
updatePlayPauseIcon();
resetControlsTimeout();
// hide bigplay overlay style
if (bigPlayOverlay) bigPlayOverlay.style.opacity = '0';
);
video.addEventListener('pause', () =>
updatePlayPauseIcon();
// force controls visible when paused
const controlsBar = document.querySelector('.custom-controls');
controlsBar.style.opacity = '1';
controlsBar.style.transform = 'translateY(0)';
if (controlsTimeout) clearTimeout(controlsTimeout);
if (bigPlayOverlay) bigPlayOverlay.style.opacity = '0.6';
);
video.addEventListener('timeupdate', updateProgress);
video.addEventListener('loadedmetadata', () =>
updateProgress();
// set initial volume display
volumeSlider.value = video.volume;
updateVolumeIcon();
);
video.addEventListener('waiting', () => showLoading(true));
video.addEventListener('canplay', () => showLoading(false));
video.addEventListener('playing', () => showLoading(false));
video.addEventListener('volumechange', () =>
volumeSlider.value = video.muted ? 0 : video.volume;
updateVolumeIcon();
);
video.addEventListener('ended', () =>
updatePlayPauseIcon();
// optional reset progress? no, keep final frame.
);
// play/pause button
playPauseBtn.addEventListener('click', togglePlayPause);
// progress bar seeking
progressBarBg.addEventListener('click', (e) =>
seekTo(e);
resetControlsTimeout();
);
progressBarBg.addEventListener('mousedown', (e) =>
isDraggingProgress = true;
seekTo(e);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
);
function onMouseMove(e)
if (isDraggingProgress)
seekTo(e);
resetControlsTimeout();
function onMouseUp()
isDraggingProgress = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
resetControlsTimeout();
// volume controls
volumeSlider.addEventListener('input', (e) =>
setVolume(e.target.value);
resetControlsTimeout();
);
volumeBtn.addEventListener('click', () =>
toggleMute();
resetControlsTimeout();
);
// speed select
speedSelect.addEventListener('change', updatePlaybackSpeed);
// fullscreen
fullscreenBtn.addEventListener('click', () =>
toggleFullscreen();
resetControlsTimeout();
);
// click on video toggles play/pause
video.addEventListener('click', handleVideoClick);
// big play overlay click (transparent region also)
bigPlayOverlay.addEventListener('click', (e) =>
e.stopPropagation();
togglePlayPause();
);
// show controls on mouse move over wrapper
videoWrapper.addEventListener('mousemove', () =>
showControlsTemporarily();
);
videoWrapper.addEventListener('mouseleave', () =>
if (!video.paused && !isDraggingProgress)
const controlsBar = document.querySelector('.custom-controls');
controlsBar.style.opacity = '0';
controlsBar.style.transform = 'translateY(12px)';
else if (video.paused)
// keep visible if paused
const controlsBar = document.querySelector('.custom-controls');
controlsBar.style.opacity = '1';
controlsBar.style.transform = 'translateY(0)';
if (controlsTimeout) clearTimeout(controlsTimeout);
);
// Fix for when fullscreen changes, controls reappearance
document.addEventListener('fullscreenchange', () =>
const controlsBar = document.querySelector('.custom-controls');
controlsBar.style.opacity = '1';
controlsBar.style.transform = 'translateY(0)';
setTimeout(() => resetControlsTimeout(), 200);
);
// ---- initial setup and fallback for poster / video ----
function setupInitial()
// set default volume from slider
video.volume = 0.8;
video.muted = false;
volumeSlider.value = 0.8;
updateVolumeIcon();
updatePlayPauseIcon();
// preload metadata: ensure duration
if (video.readyState >= 1)
updateProgress();
else
video.addEventListener('loadeddata', updateProgress);
// loading spinner visibility initial
showLoading(false);
// big play overlay initial appearance (faded)
bigPlayOverlay.style.opacity = '0.6';
// set custom controls bar transition
const controlsBar = document.querySelector('.custom-controls');
controlsBar.style.transition = 'opacity 0.25s ease, transform 0.25s ease';
controlsBar.style.opacity = '1';
// autoplay not forced, but we can set a small poster placeholder if needed.
// if video fails to load due to CORS? but sample is public.
video.addEventListener('error', (e) =>
console.warn("Video source error, fallback message:", e);
timeDisplay.textContent = "0:00 / err";
);
initEventListeners();
setupInitial();
// handle window resize (for progress bar consistency)
window.addEventListener('resize', () =>
if (!isDraggingProgress) updateProgress();
);
)();
</script>
</body>
</html>