They styled it dark, sleek, and responsive:
.player
width: 80%;
max-width: 800px;
margin: auto;
background: black;
border-radius: 12px;
overflow: hidden;
position: relative;
video
width: 100%;
display: block;
.controls
background: rgba(0,0,0,0.7);
padding: 8px;
display: flex;
align-items: center;
gap: 12px;
color: white;
button
background: none;
border: none;
color: white;
cursor: pointer;
font-size: 1.2rem;
input[type="range"]
flex: 1;
height: 4px;
cursor: pointer;
They started with a clean container:
<div class="player">
<video id="video" src="https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4" poster="https://via.placeholder.com/640x360?text=Preview"></video>
<div class="controls">
<button id="playPauseBtn">▶ Play</button>
<input type="range" id="progressBar" value="0" step="0.01">
<span id="timeDisplay">0:00 / 0:00</span>
<button id="fullscreenBtn">⛶</button>
</div>
</div>
Implementing a custom YouTube HTML5 video player on platforms like CodePen typically involves transitioning from a standard embed to the YouTube IFrame Player API. This approach allows developers to build a unique UI—using HTML, CSS, and JavaScript—that programmatically controls the video playback while maintaining compliance with YouTube's Terms of Service . Core Implementation Architecture
A custom player built on CodePen generally follows a three-tier technical structure:
HTML Structure: Instead of a direct iframe, a container (usually a Loading the API: The script Initialization: The Event Listeners: Developers use Styling (CSS): CSS is used to "skin" the player. Common techniques include hiding the default YouTube controls by setting the Using this custom setup on CodePen enables several advanced features: Below is a concise, practical article you can paste into CodePen (HTML, CSS, JS panels) to build a YouTube-like HTML5 video player with custom controls: play/pause, seek bar with progress and buffer, volume, mute, playback speed, fullscreen, and keyboard shortcuts. The code is accessible-friendly and lightweight. You can see and fork the final version here:https://www.youtube.com/iframe_api must be loaded asynchronously .onYouTubeIframeAPIReady function is defined to instantiate a YT.Player object, targeting the placeholder ID .onReady and onStateChange to synchronize the custom UI with the video's status (e.g., updating a play button icon when the video starts) .controls parameter to 0 in the API settings, allowing the custom HTML controls to take visual precedence . Key Technical Capabilities<!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>YouTube Style HTML5 Video Player | Custom Controls | CodePen</title>
<style>
*
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none; /* avoid accidental text selection on double-click */
body
background: linear-gradient(145deg, #0a0f1c 0%, #0c1222 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
padding: 24px;
/* main card container */
.player-container
max-width: 1000px;
width: 100%;
background: #000000;
border-radius: 28px;
box-shadow: 0 25px 45px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
overflow: hidden;
transition: all 0.2s ease;
/* video wrapper - keeps aspect ratio 16:9 */
.video-wrapper
position: relative;
width: 100%;
background: #000;
cursor: pointer;
.video-wrapper video
width: 100%;
height: auto;
display: block;
vertical-align: middle;
/* custom controls bar - YouTube inspired */
.custom-controls
background: rgba(20, 20, 28, 0.92);
backdrop-filter: blur(12px);
padding: 12px 18px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
transition: opacity 0.2s;
border-top: 1px solid rgba(255, 255, 255, 0.1);
/* left group: play/pause + time + volume */
.controls-left
display: flex;
align-items: center;
gap: 14px;
flex: 2;
/* center group: progress bar */
.controls-center
flex: 6;
min-width: 140px;
/* right group: speed, pip, fullscreen */
.controls-right
display: flex;
align-items: center;
gap: 14px;
flex: 2;
justify-content: flex-end;
/* icon buttons */
.ctrl-btn
background: transparent;
border: none;
color: #f1f3f4;
font-size: 20px;
cursor: pointer;
width: 36px;
height: 36px;
border-radius: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-weight: 500;
.ctrl-btn:hover
background-color: rgba(255, 255, 255, 0.15);
transform: scale(1.02);
.ctrl-btn:active
transform: scale(0.96);
/* time display */
.time-display
font-family: 'Monaco', 'Cascadia Code', monospace;
font-size: 14px;
font-weight: 500;
background: rgba(0,0,0,0.6);
padding: 6px 12px;
border-radius: 32px;
letter-spacing: 0.5px;
color: #e0e0e0;
/* volume slider container */
.volume-container
display: flex;
align-items: center;
gap: 8px;
.volume-slider
width: 80px;
height: 4px;
-webkit-appearance: none;
background: rgba(255,255,255,0.3);
border-radius: 4px;
outline: none;
cursor: pointer;
.volume-slider::-webkit-slider-thumb
-webkit-appearance: none;
width: 12px;
height: 12px;
background: #ff0000;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 2px white;
border: none;
/* progress bar */
.progress-bar
display: flex;
align-items: center;
gap: 10px;
width: 100%;
.progress-track
flex: 1;
height: 5px;
background: rgba(255,255,255,0.25);
border-radius: 5px;
cursor: pointer;
position: relative;
transition: height 0.1s;
.progress-track:hover
height: 7px;
.progress-filled
width: 0%;
height: 100%;
background: #ff0000;
border-radius: 5px;
position: relative;
pointer-events: none;
.progress-buffer
position: absolute;
top: 0;
left: 0;
height: 100%;
background: rgba(255,255,255,0.4);
border-radius: 5px;
pointer-events: none;
width: 0%;
/* speed dropdown custom */
.speed-dropdown
position: relative;
.speed-btn
background: rgba(30,30,38,0.9);
border-radius: 24px;
padding: 0 12px;
font-size: 13px;
font-weight: 600;
width: auto;
gap: 4px;
letter-spacing: 0.3px;
.speed-menu
position: absolute;
bottom: 45px;
right: 0;
background: #1e1e2a;
backdrop-filter: blur(16px);
border-radius: 12px;
padding: 8px 0;
min-width: 100px;
display: none;
flex-direction: column;
box-shadow: 0 8px 20px rgba(0,0,0,0.4);
border: 1px solid rgba(255,255,255,0.1);
z-index: 20;
.speed-menu button
background: transparent;
border: none;
color: white;
padding: 8px 16px;
text-align: left;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background 0.1s;
.speed-menu button:hover
background: #ff0000aa;
.speed-menu.show
display: flex;
/* small responsiveness */
@media (max-width: 640px)
.custom-controls
padding: 10px 12px;
gap: 8px;
flex-wrap: wrap;
.controls-left, .controls-right
flex: auto;
.volume-slider
width: 60px;
.ctrl-btn
width: 32px;
height: 32px;
font-size: 18px;
.time-display
font-size: 11px;
padding: 4px 8px;
</style>
</head>
<body>
<div class="player-container">
<div class="video-wrapper">
<!-- HTML5 video element - using a high quality sample video (Big Buck Bunny short snippet)
This is a public domain / creative commons video from Blender Foundation,
directly accessible via reliable CDN. It's fully legal for demo purposes. -->
<video id="videoPlayer" preload="metadata" poster="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg">
<source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" type="video/mp4">
Your browser does not support HTML5 video.
</video>
</div>
<div class="custom-controls">
<!-- left section -->
<div class="controls-left">
<button class="ctrl-btn" id="playPauseBtn" aria-label="Play/Pause">▶</button>
<div class="time-display">
<span id="currentTime">0:00</span> / <span id="duration">0:00</span>
</div>
<div class="volume-container">
<button class="ctrl-btn" 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>
<!-- center progress bar -->
<div class="controls-center">
<div class="progress-bar">
<div class="progress-track" id="progressTrack">
<div class="progress-buffer" id="bufferIndicator"></div>
<div class="progress-filled" id="progressFilled"></div>
</div>
</div>
</div>
<!-- right section: speed, pip, fullscreen -->
<div class="controls-right">
<div class="speed-dropdown">
<button class="ctrl-btn speed-btn" id="speedBtn">1x ▼</button>
<div class="speed-menu" id="speedMenu">
<button data-speed="0.5">0.5x</button>
<button data-speed="0.75">0.75x</button>
<button data-speed="1">1x</button>
<button data-speed="1.25">1.25x</button>
<button data-speed="1.5">1.5x</button>
<button data-speed="2">2x</button>
</div>
</div>
<button class="ctrl-btn" id="pipBtn" aria-label="Picture in Picture">📺</button>
<button class="ctrl-btn" id="fullscreenBtn" aria-label="Fullscreen">⛶</button>
</div>
</div>
</div>
<script>
(function()
// DOM elements
const video = document.getElementById('videoPlayer');
const playPauseBtn = document.getElementById('playPauseBtn');
const currentTimeSpan = document.getElementById('currentTime');
const durationSpan = document.getElementById('duration');
const progressTrack = document.getElementById('progressTrack');
const progressFilled = document.getElementById('progressFilled');
const bufferIndicator = document.getElementById('bufferIndicator');
const volumeSlider = document.getElementById('volumeSlider');
const muteBtn = document.getElementById('muteBtn');
const fullscreenBtn = document.getElementById('fullscreenBtn');
const pipBtn = document.getElementById('pipBtn');
const speedBtn = document.getElementById('speedBtn');
const speedMenu = document.getElementById('speedMenu');
// helper: format 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 time displays and progress
function updateTimeAndProgress()
if (video.duration && !isNaN(video.duration))
const current = video.currentTime;
const percent = (current / video.duration) * 100;
progressFilled.style.width = `$percent%`;
currentTimeSpan.textContent = formatTime(current);
else
currentTimeSpan.textContent = "0:00";
// update buffer progress
function updateBufferProgress()
if (!video.buffered.length) return;
const duration = video.duration;
if (!duration
// set video duration label
function setDuration()
if (video.duration && !isNaN(video.duration))
durationSpan.textContent = formatTime(video.duration);
else
durationSpan.textContent = "0:00";
// Play/Pause toggle
function togglePlayPause()
if (video.paused)
video.play();
playPauseBtn.textContent = "⏸";
else
video.pause();
playPauseBtn.textContent = "▶";
// update play button icon on play/pause events
function updatePlayIcon()
playPauseBtn.textContent = video.paused ? "▶" : "⏸";
// seek when clicking on progress bar
function seek(event)
const rect = progressTrack.getBoundingClientRect();
const clickX = event.clientX - rect.left;
const width = rect.width;
const percent = Math.min(Math.max(clickX / width, 0), 1);
if (video.duration && !isNaN(video.duration))
video.currentTime = percent * video.duration;
// Volume control
function setVolume(value)
let vol = parseFloat(value);
if (isNaN(vol)) vol = 1;
vol = Math.min(Math.max(vol, 0), 1);
video.volume = vol;
volumeSlider.value = vol;
updateMuteIcon();
function updateMuteIcon() video.volume === 0)
muteBtn.textContent = "🔇";
else if (video.volume < 0.3)
muteBtn.textContent = "🔈";
else if (video.volume < 0.7)
muteBtn.textContent = "🔉";
else
muteBtn.textContent = "🔊";
function toggleMute()
if (video.muted)
video.muted = false;
// restore volume from slider if volume was 0?
if (video.volume === 0) setVolume(0.5);
else
video.muted = true;
updateMuteIcon();
if (!video.muted) volumeSlider.value = video.volume;
else volumeSlider.value = 0;
// Fullscreen handling
function toggleFullscreen()
const container = document.querySelector('.player-container');
if (!document.fullscreenElement)
container.requestFullscreen().catch(err =>
console.warn(`Fullscreen error: $err.message`);
);
else
document.exitFullscreen();
// Picture-in-Picture (modern API)
async function togglePictureInPicture()
try
if (document.pictureInPictureElement)
await document.exitPictureInPicture();
else if (document.pictureInPictureEnabled)
await video.requestPictureInPicture();
else
alert("PiP not supported in this browser");
catch (error)
console.error("PiP error:", error);
// Speed handling
function setPlaybackSpeed(rate)
video.playbackRate = rate;
speedBtn.textContent = `$ratex ▼`;
// close menu after selection
speedMenu.classList.remove('show');
// update buffer periodically
function handleProgress()
updateBufferProgress();
updateTimeAndProgress();
// Event listeners
playPauseBtn.addEventListener('click', togglePlayPause);
video.addEventListener('play', updatePlayIcon);
video.addEventListener('pause', updatePlayIcon);
video.addEventListener('timeupdate', updateTimeAndProgress);
video.addEventListener('loadedmetadata', () =>
setDuration();
updateTimeAndProgress();
updateBufferProgress();
);
video.addEventListener('progress', updateBufferProgress);
video.addEventListener('seeked', updateTimeAndProgress);
video.addEventListener('waiting', () => /* optional loading indicator not needed */ );
progressTrack.addEventListener('click', seek);
volumeSlider.addEventListener('input', (e) =>
video.muted = false;
setVolume(e.target.value);
);
muteBtn.addEventListener('click', toggleMute);
video.addEventListener('volumechange', () =>
if (!video.muted) volumeSlider.value = video.volume;
else volumeSlider.value = 0;
updateMuteIcon();
);
fullscreenBtn.addEventListener('click', toggleFullscreen);
pipBtn.addEventListener('click', togglePictureInPicture);
// speed dropdown logic
speedBtn.addEventListener('click', (e) =>
e.stopPropagation();
speedMenu.classList.toggle('show');
);
// close menu on clicking outside
document.addEventListener('click', (e) =>
if (!speedBtn.contains(e.target) && !speedMenu.contains(e.target))
speedMenu.classList.remove('show');
);
// speed options
const speedOptions = speedMenu.querySelectorAll('button');
speedOptions.forEach(btn =>
btn.addEventListener('click', (e) =>
e.stopPropagation();
const speedVal = parseFloat(btn.getAttribute('data-speed'));
if (!isNaN(speedVal)) setPlaybackSpeed(speedVal);
speedMenu.classList.remove('show');
);
);
// initial mute icon
updateMuteIcon();
setVolume(1);
// extra: if video metadata loads late, set duration again
video.addEventListener('canplay', () =>
setDuration();
updateBufferProgress();
);
// when duration changes (some streams)
video.addEventListener('durationchange', setDuration);
// double click video to toggle fullscreen (like YouTube)
const videoWrapper = document.querySelector('.video-wrapper');
videoWrapper.addEventListener('dblclick', (e) =>
e.stopPropagation();
toggleFullscreen();
);
// also single click on video toggles play/pause
video.addEventListener('click', (e) =>
e.stopPropagation();
togglePlayPause();
);
// handle keyboard shortcuts (space, k, f, etc)
window.addEventListener('keydown', (e) => );
// sync progress bar on load and when seeking via keyboard
video.addEventListener('seeked', () =>
updateTimeAndProgress();
);
// optional: show loading state? not needed for demo but nice
// preload initial buffer display
setInterval(() => video.buffered.length) updateBufferProgress();
, 300);
)();
</script>
</body>
</html>
👉 Custom YouTube-style HTML5 Video Player on CodePen
youtube html5 video player codepen