<!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) =>
const tag = e.target.tagName;
if (tag === 'INPUT' );
// 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(() => , 300);
)();
</script>
</body>
</html>
Building a YouTube-style HTML5 video player on CodePen is a great way to learn web development. This guide covers how to set up a basic player using native HTML5 tags and how to integrate actual YouTube videos. 1. Set Up Your Environment on CodePen Before writing code, prepare your workspace:
Create a New Pen: Go to CodePen.io and click "Pen" to start a new project.
Configure Settings: If you want a specific look, you can add external libraries like Font Awesome in the CSS settings to use icons for play/pause buttons.
Live Preview: CodePen will automatically show your results in the bottom pane as you type. 2. Creating a Native HTML5 Video Player To build a player from scratch using the HTML5 tag:
HTML Structure: Use the tag. You can add the controls attribute to use the browser's default player interface.
Use code with caution. Copied to clipboard
Responsive Design: To make your player fit different screens, set the CSS width to 100% and height to auto.
Custom Controls: To create a custom "YouTube-like" interface, omit the controls attribute and build your own buttons using JavaScript to interact with the HTMLMediaElement API (e.g., video.play(), video.pause()). 3. Integrating YouTube Videos
You cannot use the tag directly for YouTube URLs because YouTube serves content via its own player. Instead, use an :
Embedding: Use the official YouTube embed code found under the "Share" button on any YouTube video.
Use code with caution. Copied to clipboard
Advanced Styling: Use libraries like Plyr or Video.js on CodePen to wrap YouTube videos in a highly customizable HTML5-style interface. 4. Local Coding Workshops
If you prefer hands-on learning, check out these upcoming tech workshops: Teen Tech Hub: Website Building Date & Time: Thursday, April 30, 2026, at 4:00 PM
Venue: Homewood Public Library, 1721 Oxmoor Road, Birmingham, AL 35209
Description: A workshop focused on learning the basics of building websites. Cost: Free (contact library for details) Learn to Code with AI & Entertainment Date & Time: Wednesday, April 29, 2026, at 4:30 PM
Venue: Bletchley Commons, 411 University Ridge, Greenville, SC 29601
Description: Teaches coding concepts through real-world pop culture data like movies and video games. No prior experience required. Tickets: Event Details Expand map
Developing a custom YouTube player using HTML5 and CSS on CodePen is a fantastic way to sharpen your front-end skills. By leveraging the YouTube IFrame Player API, you can go beyond a simple embed and create unique, branded experiences. 🚀 The Core Concepts
To build this, you need three primary components working together:
HTML: A container for the IFrame and custom control buttons.
CSS: Styling to hide default UI or wrap the player in a custom skin.
JavaScript: The logic that communicates with the YouTube API. 🛠️ Step-by-Step Implementation 1. The HTML Structure
You need a
Use code with caution. Copied to clipboard 2. The JavaScript (The "Brain")
You must load the IFrame Player API script and define the onYouTubeIframeAPIReady function. javascript
// 1. Load the API script asynchronously var tag = document.createElement('script'); tag.src = "https://youtube.com"; var firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); // 2. Create the player object var player; function onYouTubeIframeAPIReady() player = new YT.Player('player', height: '360', width: '640', videoId: 'dQw4w9WgXcQ', // Replace with your video ID playerVars: 'controls': 0, // Hides default YouTube controls 'rel': 0 ); // 3. Hook up your custom buttons document.getElementById('play-btn').addEventListener('click', () => player.playVideo()); document.getElementById('pause-btn').addEventListener('click', () => player.pauseVideo()); Use code with caution. Copied to clipboard 3. Styling with CSS
Use CSS to ensure the video is responsive and your controls look sleek. Use code with caution. Copied to clipboard 💡 Why Use CodePen for This? Instant Preview: See CSS changes in real-time.
External Assets: Easily link the YouTube API in the Pen settings.
Forking: Find existing "YouTube Player" Pens and "fork" them to learn from others' code. ⚠️ Key Technical Limits
Autoplay: Most browsers block autoplay with sound; ensure the video is muted if you want it to start automatically.
Mobile: Custom volume controls often don't work on iOS/Android due to OS-level restrictions.
Privacy: Use ://youtube-nocookie.com in your API calls for better user privacy. Provide a dark mode CSS theme for the controls?
<!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 | CodePen Concept</title>
<!-- Google Fonts for a modern touch (optional but clean) -->
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600&display=swap" rel="stylesheet">
<!-- Font Awesome 6 (Free Icons) for controls -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
*
margin: 0;
padding: 0;
box-sizing: border-box;
body
background: linear-gradient(145deg, #0f0f0f 0%, #1a1a1a 100%);
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
/* card-like container with subtle glass effect */
.player-container
max-width: 1100px;
width: 100%;
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(2px);
border-radius: 2rem;
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: 16:9 aspect ratio for true YouTube feel */
.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(28, 28, 28, 0.95);
backdrop-filter: blur(10px);
padding: 0.75rem 1rem;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.8rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
transition: opacity 0.2s;
/* left group */
.controls-left
display: flex;
align-items: center;
gap: 0.9rem;
/* center group (progress) takes flexible space */
.controls-center
flex: 1;
min-width: 120px;
display: flex;
align-items: center;
gap: 0.8rem;
/* right group (time, volume, settings like quality/playback speed) */
.controls-right
display: flex;
align-items: center;
gap: 1rem;
/* buttons style */
.ctrl-btn
background: transparent;
border: none;
color: #f1f1f1;
font-size: 1.2rem;
cursor: pointer;
padding: 0.4rem;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
width: 36px;
height: 36px;
.ctrl-btn:hover
background-color: rgba(255, 255, 255, 0.15);
transform: scale(1.02);
.ctrl-btn:active
transform: scale(0.96);
/* volume slider container */
.volume-container
display: flex;
align-items: center;
gap: 0.4rem;
.volume-slider
width: 80px;
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: #ff0000;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 2px white;
/* progress bar (seek) */
.progress-bar-container
flex: 1;
display: flex;
align-items: center;
cursor: pointer;
position: relative;
.progress-bg
background: rgba(255, 255, 255, 0.25);
height: 5px;
width: 100%;
border-radius: 5px;
position: relative;
transition: height 0.1s;
.progress-fill
background: #ff0000;
width: 0%;
height: 100%;
border-radius: 5px;
position: relative;
transition: width 0.05s linear;
.progress-bg:hover
height: 7px;
/* time text */
.time-text
font-size: 0.85rem;
font-weight: 500;
color: #ddd;
letter-spacing: 0.3px;
font-family: monospace;
/* speed & quality dropdown yt-like */
.settings-dropdown
position: relative;
.speed-btn, .quality-btn
background: rgba(0,0,0,0.6);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 20px;
padding: 0.3rem 0.7rem;
font-size: 0.75rem;
font-weight: 500;
color: white;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.3rem;
transition: background 0.15s;
.speed-btn:hover, .quality-btn:hover
background: rgba(255,255,255,0.2);
.dropdown-menu
position: absolute;
bottom: 40px;
right: 0;
background: #212121;
border-radius: 12px;
padding: 0.5rem 0;
min-width: 130px;
box-shadow: 0 8px 20px rgba(0,0,0,0.5);
z-index: 20;
display: none;
flex-direction: column;
border: 1px solid #3e3e3e;
.dropdown-menu span
padding: 0.5rem 1rem;
font-size: 0.8rem;
color: #eee;
cursor: pointer;
transition: background 0.1s;
.dropdown-menu span:hover
background: #3a3a3a;
.dropdown-menu span.active-speed, .dropdown-menu span.active-quality
color: #ff5e5e;
font-weight: 600;
background: #2a2a2a;
/* fullscreen icon adjustment */
.fullscreen-icon
font-size: 1.3rem;
/* Responsive */
@media (max-width: 700px)
.custom-controls
flex-wrap: wrap;
gap: 0.5rem;
.controls-center
order: 3;
width: 100%;
margin-top: 0.3rem;
.volume-slider
width: 60px;
.ctrl-btn
width: 32px;
height: 32px;
font-size: 1rem;
</style>
</head>
<body>
<div class="player-container">
<div class="video-wrapper">
<!-- HTML5 Video element - using a high-quality sample video (Big Buck Bunny trailer, public domain / creative commons)
This is a direct, reliable video file that works cross-browser. -->
<video id="videoPlayer" 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 the video tag.
</video>
</div>
<div class="custom-controls">
<!-- Left group: play/pause, volume -->
<div class="controls-left">
<button class="ctrl-btn" id="playPauseBtn" aria-label="Play/Pause">
<i class="fas fa-play" id="playIcon"></i>
</button>
<div class="volume-container">
<button class="ctrl-btn" id="muteBtn" aria-label="Mute">
<i class="fas fa-volume-up" id="volumeIcon"></i>
</button>
<input type="range" id="volumeSlider" class="volume-slider" min="0" max="1" step="0.02" value="1">
</div>
</div>
<!-- Center group: seek bar + time -->
<div class="controls-center">
<div class="progress-bar-container" id="progressContainer">
<div class="progress-bg">
<div class="progress-fill" id="progressFill"></div>
</div>
</div>
<div class="time-text">
<span id="currentTimeDisplay">0:00</span> / <span id="durationDisplay">0:00</span>
</div>
</div>
<!-- Right group: playback speed, quality (simulated), fullscreen -->
<div class="controls-right">
<div class="settings-dropdown" id="speedDropdownContainer">
<button class="speed-btn" id="speedBtn">Speed <i class="fas fa-chevron-down"></i></button>
<div class="dropdown-menu" id="speedMenu">
<span data-speed="0.5">0.5x</span>
<span data-speed="0.75">0.75x</span>
<span data-speed="1" class="active-speed">1x</span>
<span data-speed="1.25">1.25x</span>
<span data-speed="1.5">1.5x</span>
<span data-speed="2">2x</span>
</div>
</div>
<div class="settings-dropdown" id="qualityDropdownContainer">
<button class="quality-btn" id="qualityBtn">Quality <i class="fas fa-chevron-down"></i></button>
<div class="dropdown-menu" id="qualityMenu">
<span data-quality="auto" class="active-quality">Auto (720p)</span>
<span data-quality="hd">720p (HD)</span>
<span data-quality="sd">480p</span>
<!-- Since the source is a single mp4, we demonstrate quality simulation: switches source behavior
but for demo we show capability; we'll implement visual or actual source change? We'll simulate quality change via adjusting video source?
Actually Big Buck Bunny has only one quality. For demo purposes, we'll implement a simulated quality selector that shows a toast-like indication.
However, to be more robust I'll provide a true dynamic source quality? we can use another source? but that would break reliability.
let's implement 'simulated quality change' plus optional note: but user expects a demo, we show that we can handle quality switching logic with custom sources.
For reliable presentation, and not breaking, I'll implement quality notification + actually keep same video but transform visual? better to provide alternate source?
I'll add an optional 'switchQuality' that tells visually but does not break; ideal for codepen demonstration: shows concept of selecting quality.
-->
</div>
</div>
<button class="ctrl-btn" id="fullscreenBtn" aria-label="Fullscreen">
<i class="fas fa-expand fullscreen-icon"></i>
</button>
</div>
</div>
</div>
<script>
(function()
// DOM elements
const video = document.getElementById('videoPlayer');
const playPauseBtn = document.getElementById('playPauseBtn');
const playIcon = document.getElementById('playIcon');
const muteBtn = document.getElementById('muteBtn');
const volumeIcon = document.getElementById('volumeIcon');
const volumeSlider = document.getElementById('volumeSlider');
const progressContainer = document.getElementById('progressContainer');
const progressFill = document.getElementById('progressFill');
const currentTimeSpan = document.getElementById('currentTimeDisplay');
const durationSpan = document.getElementById('durationDisplay');
const fullscreenBtn = document.getElementById('fullscreenBtn');
// speed elements
const speedBtn = document.getElementById('speedBtn');
const speedMenu = document.getElementById('speedMenu');
const qualityBtn = document.getElementById('qualityBtn');
const qualityMenu = document.getElementById('qualityMenu');
let isDraggingSeek = false;
// Helper: format time (seconds -> 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 bar and time display
function updateProgress()
if (!isDraggingSeek)
const percent = (video.currentTime / video.duration) * 100;
progressFill.style.width = `$percent%`;
currentTimeSpan.textContent = formatTime(video.currentTime);
function setVideoDuration()
if (video.duration && isFinite(video.duration))
durationSpan.textContent = formatTime(video.duration);
else
durationSpan.textContent = "0:00";
// Play/Pause logic
function togglePlayPause()
function updatePlayPauseIcon()
if (video.paused)
playIcon.classList.remove('fa-pause');
playIcon.classList.add('fa-play');
else
playIcon.classList.remove('fa-play');
playIcon.classList.add('fa-pause');
// Volume & mute
function updateVolumeIcon(value)
function setVolume(value)
let vol = parseFloat(value);
if (isNaN(vol)) vol = 1;
vol = Math.min(1, Math.max(0, vol));
video.volume = vol;
volumeSlider.value = vol;
if (vol === 0)
video.muted = true;
updateVolumeIcon(0);
else
if (video.muted) video.muted = false;
updateVolumeIcon(vol);
function toggleMute()
if (video.muted)
video.muted = false;
setVolume(video.volume);
else
video.muted = true;
updateVolumeIcon(0);
// Seek logic
function seek(e)
const rect = progressContainer.getBoundingClientRect();
let clickX = e.clientX - rect.left;
let width = rect.width;
let percent = Math.min(Math.max(0, clickX / width), 1);
if (video.duration)
video.currentTime = percent * video.duration;
progressFill.style.width = `$percent * 100%`;
function startDragSeek(e)
isDraggingSeek = true;
seek(e);
window.addEventListener('mousemove', onDragSeek);
window.addEventListener('mouseup', stopDragSeek);
function onDragSeek(e)
if (isDraggingSeek)
seek(e);
function stopDragSeek()
isDraggingSeek = false;
window.removeEventListener('mousemove', onDragSeek);
window.removeEventListener('mouseup', stopDragSeek);
// Fullscreen API
function toggleFullscreen()
const container = document.querySelector('.player-container');
if (!document.fullscreenElement)
container.requestFullscreen().catch(err =>
console.warn(`Fullscreen error: $err.message`);
);
else
document.exitFullscreen();
// Speed change
function setPlaybackSpeed(speed)
video.playbackRate = parseFloat(speed);
// update active class in dropdown
const items = speedMenu.querySelectorAll('span');
items.forEach(item =>
if (item.getAttribute('data-speed') == speed)
item.classList.add('active-speed');
else
item.classList.remove('active-speed');
);
speedBtn.innerHTML = `Speed $speedx <i class="fas fa-chevron-down"></i>`;
// Quality simulation (because video only has one src, but we demonstrate a UI concept with cosmetic feedback)
// In a real scenario you'd change the video source. For this demo, we provide a notification-like logic but won't break the experience.
// Also we show the 'active-quality' toggles and show a temporary tooltip. It's a codepen concept after all.
function setQuality(qualityLevel)
// For demo: show simple alert replacement using a floating notification? but better to console + update UI active.
const items = qualityMenu.querySelectorAll('span');
let selectedText = '';
items.forEach(item =>
const q = item.getAttribute('data-quality');
if (q === qualityLevel)
item.classList.add('active-quality');
selectedText = item.innerText;
else
item.classList.remove('active-quality');
);
qualityBtn.innerHTML = `$ <i class="fas fa-chevron-down"></i>`;
// Since it's a demo and original source is fixed ~720p, we just show a subtle UI message without interrupting.
// However, you could implement dynamic source switch if you had multiple qualities. We provide a small console feedback.
console.log(`[Quality UI] Selected quality: $selectedText (simulated - original video remains same source for demo stability)`);
// Optional: show mini tooltip / temporary popup in corner? For better UX create small transient message.
showToast(`Quality set to $selectedText (simulated)`);
// Simple toast (non-intrusive)
function showToast(msg)
let toast = document.querySelector('.toast-message');
if(!toast)
toast = document.createElement('div');
toast.className = 'toast-message';
toast.style.position = 'fixed';
toast.style.bottom = '20px';
toast.style.left = '50%';
toast.style.transform = 'translateX(-50%)';
toast.style.backgroundColor = '#212121e6';
toast.style.backdropFilter = 'blur(12px)';
toast.style.color = 'white';
toast.style.padding = '8px 18px';
toast.style.borderRadius = '40px';
toast.style.fontSize = '0.8rem';
toast.style.fontWeight = '500';
toast.style.zIndex = '9999';
toast.style.fontFamily = 'Inter, sans-serif';
toast.style.pointerEvents = 'none';
toast.style.border = '1px solid #ff5e5e66';
document.body.appendChild(toast);
toast.textContent = msg;
toast.style.opacity = '1';
clearTimeout(window.toastTimeout);
window.toastTimeout = setTimeout(() =>
toast.style.opacity = '0';
, 1800);
// Dropdowns logic (click outside close)
function setupDropdown(btn, menu)
btn.addEventListener('click', (e) =>
e.stopPropagation();
const isOpen = menu.style.display === 'flex';
// close all other dropdowns first
document.querySelectorAll('.dropdown-menu').forEach(m => m.style.display = 'none');
if (!isOpen)
menu.style.display = 'flex';
else
menu.style.display = 'none';
);
// clicking inside menu should not close immediately, but after selection we close
menu.addEventListener('click', (e) =>
e.stopPropagation();
);
// Close dropdowns when clicking elsewhere
window.addEventListener('click', () =>
document.querySelectorAll('.dropdown-menu').forEach(menu => menu.style.display = 'none');
);
// Event Listeners
playPauseBtn.addEventListener('click', togglePlayPause);
video.addEventListener('play', updatePlayPauseIcon);
video.addEventListener('pause', updatePlayPauseIcon);
video.addEventListener('ended', () =>
playIcon.classList.remove('fa-pause');
playIcon.classList.add('fa-play');
);
volumeSlider.addEventListener('input', (e) =>
setVolume(e.target.value);
);
muteBtn.addEventListener('click', toggleMute);
video.addEventListener('volumechange', () =>
if (video.muted) updateVolumeIcon(0);
else updateVolumeIcon(video.volume);
volumeSlider.value = video.muted ? 0 : video.volume;
);
video.addEventListener('timeupdate', updateProgress);
video.addEventListener('loadedmetadata', () =>
setVideoDuration();
updateProgress();
);
video.addEventListener('durationchange', setVideoDuration);
progressContainer.addEventListener('mousedown', startDragSeek);
// click on progress bar also seeks
progressContainer.addEventListener('click', (e) =>
if (!isDraggingSeek) seek(e);
);
fullscreenBtn.addEventListener('click', toggleFullscreen);
// Speed menu items
const speedOptions = speedMenu.querySelectorAll('span');
speedOptions.forEach(opt =>
opt.addEventListener('click', (e) =>
const spd = opt.getAttribute('data-speed');
setPlaybackSpeed(spd);
speedMenu.style.display = 'none';
showToast(`Playback speed: $spdx`);
e.stopPropagation();
);
);
// Quality menu items
const qualityOptions = qualityMenu.querySelectorAll('span');
qualityOptions.forEach(opt =>
opt.addEventListener('click', (e) =>
const qualityVal = opt.getAttribute('data-quality');
setQuality(qualityVal);
qualityMenu.style.display = 'none';
e.stopPropagation();
);
);
// Initially set volume icon
updateVolumeIcon(video.volume);
volumeSlider.value = video.volume;
setPlaybackSpeed(1);
// Default selected quality active
setQuality('auto');
// Enable video click to play/pause
const videoWrapper = document.querySelector('.video-wrapper');
videoWrapper.addEventListener('click', (e) => videoWrapper.contains(e.target))
togglePlayPause();
);
// Additional: Ensure progress bar updates on load
if(video.readyState >= 1)
setVideoDuration();
// Handle initial poster fallback? all good.
// preload hint, set metadata
video.preload = 'metadata';
)();
</script>
</body>
</html>
To create a YouTube-style HTML5 video player on CodePen, you can either embed the native YouTube player using its IFrame API or build a custom player interface that wraps around a video element. Popular Implementation Approaches
YouTube IFrame API: This is the official and most reliable way to embed YouTube videos with programmatic control. You can see a live example in this Auto Play YouTube Video CodePen.
Custom Player UI: You can build your own controls (play, pause, volume, progress bar) using HTML/CSS and link them to the video using JavaScript. A comprehensive example is this Custom YouTube-like Player on CodePen.
Library Wrappers: Tools like Plyr.io provide a modern, accessible interface for both HTML5 and YouTube videos. Check out this Plyr.io YouTube Implementation. Basic Embedding Methods
If you just need the video to appear without custom logic, use one of these two methods:
Standard IFrame Embed:Copy the embed code directly from YouTube's "Share" menu.
Use code with caution. Copied to clipboard Source: YouTube Help.
HTML5 Video Tag (Advanced):Technically, the tag is for self-hosted files. To use it with YouTube, you usually need a "tech" layer like Video.js to bridge the two. An example of this can be found in this Video.js Format CodePen. Essential Features to Include
When building your own version on CodePen, aim for these key functionalities:
Responsive Container: Use a 16:9 aspect ratio wrapper to ensure the player looks good on all screens.
Custom Controls: Map your own buttons to the YouTube API's playVideo(), pauseVideo(), and setVolume() functions.
Timeline Scrubbing: Use an to create a functional progress bar that updates as the video plays. plyr.io with HTML5 Video, YouTube Video, Vimeo Video
Building Custom YouTube HTML5 Players on CodePen CodePen is a popular playground for front-end developers to experiment with the YouTube iFrame Player API youtube html5 video player codepen
, allowing them to go beyond simple embeds and create unique, branded video experiences . Since YouTube switched to HTML5 as its default player
in 2015, developers have used CSS and JavaScript to wrap these embeds in custom interfaces. Core Implementation Methods
There are two primary ways to handle YouTube videos on CodePen: Standard iFrame Embed : The simplest method involves using a standard
tag provided by YouTube's "Share > Embed" option. Developers often use this in CodePen to test responsive CSS wrappers that maintain a 16:9 aspect ratio. YouTube iFrame API
: For more control, developers use JavaScript to initialize the
object. This allows for programmatic control over play, pause, volume, and custom event listeners like onPlayerReady Codepen.io Popular Examples on CodePen
You can find various community-made players by searching tags like youtube-player html5-video on CodePen. Notable implementation styles include: YouTube Video Player - Codepen.io
build a custom YouTube HTML5 player on CodePen by utilizing the YouTube IFrame Player API
, which allows you to control the video player with JavaScript and hide standard UI elements to apply your own CSS styling. Key Features to Implement Custom Controls
element's logic to build custom play, pause, and volume buttons. Dynamic Loading : Use the YouTube API to load videos by their without reloading the page. Event Listeners : Track player states (e.g., onPlayerReady onStateChange
) to trigger custom animations or UI changes when a video ends. Responsive Resizing : Apply CSS to the
or a wrapper div to ensure the player maintains its aspect ratio across different screen sizes. Popular Implementation Methods IFrame Embedding : The simplest method where you copy the Embed Code from YouTube and paste it into your HTML. API Integration : For full control, include the script
Building Custom YouTube Players on CodePen Creating a custom YouTube HTML5 video player
allows developers to bypass the standard YouTube interface for a look that matches their site's branding. Platforms like
are ideal for prototyping these players using a combination of HTML, CSS, and the YouTube IFrame Player API 1. The Core Technology: IFrame API While HTML5 has a native
tag, it cannot directly play YouTube URLs due to licensing and formatting restrictions. Instead, YouTube uses an iframe-based HTML5 player . To build custom controls on CodePen, you must use the YouTube IFrame API
which allows JavaScript to send commands (like play, pause, or seek) to the embedded video. 2. Basic Setup on CodePen
To get started, you can follow these structural steps commonly seen in high-quality Pens: YouTube Switches to HTML5 Video Player - InfoQ
<div class="player" id="player" data-playing="false">
<video id="video" preload="metadata" crossorigin="anonymous">
<source src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" type="video/mp4">
Your browser does not support HTML5 video.
</video>
<div class="controls" id="controls" aria-hidden="false">
<button id="play" class="btn" aria-label="Play (k)">►</button>
<div class="progress-wrap" id="progressWrap" aria-label="Video progress">
<div class="buffer" id="buffer"></div>
<div class="progress" id="progress"></div>
<input type="range" id="seek" min="0" max="100" value="0" step="0.01" aria-label="Seek">
</div>
<div class="right-controls">
<button id="mute" class="btn" aria-label="Mute">🔊</button>
<input id="volume" type="range" min="0" max="1" step="0.01" value="1" aria-label="Volume">
<select id="speed" aria-label="Playback speed">
<option value="0.5">0.5×</option>
<option value="0.75">0.75×</option>
<option value="1" selected>1×</option>
<option value="1.25">1.25×</option>
<option value="1.5">1.5×</option>
<option value="2">2×</option>
</select>
<button id="fs" class="btn" aria-label="Toggle fullscreen">⛶</button>
</div>
</div>
</div>
This is where we transform a boring video into a modern YouTube clone. We need gradient overlays, smooth transitions, and hidden default controls.
/* Reset & Base */ * margin: 0; padding: 0; box-sizing: border-box;body background: #0f0f0f; /* YouTube dark theme background */ font-family: 'Roboto', system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh;
.video-container max-width: 1200px; width: 100%; background: #000; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 25px rgba(0,0,0,0.5);
.video-wrapper position: relative; width: 100%; background: #000; Building a YouTube-style HTML5 video player on CodePen
/* The Video Element */ .video-element width: 100%; height: auto; display: block; cursor: pointer;
/* Custom Controls Container / .custom-controls position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0) 100%); padding: 20px 16px 10px 16px; opacity: 0; transition: opacity 0.3s ease; pointer-events: none; / Allows clicking through to video initially */
/* Show controls when hovering over wrapper */ .video-wrapper:hover .custom-controls opacity: 1; pointer-events: auto;
/* YouTube Style Progress Bar */ .progress-container position: relative; width: 100%; height: 5px; background-color: rgba(255,255,255,0.2); border-radius: 5px; cursor: pointer; margin-bottom: 12px; transition: height 0.2s ease;
.progress-container:hover height: 8px;
.progress-buffer position: absolute; height: 100%; background-color: rgba(255,255,255,0.4); width: 0%; border-radius: 5px;
.progress-filled position: absolute; height: 100%; background-color: #ff0000; /* YouTube Red */ width: 0%; border-radius: 5px; z-index: 2;
.progress-handle position: absolute; height: 12px; width: 12px; background-color: #ff0000; border-radius: 50%; top: 50%; transform: translate(-50%, -50%); left: 0%; z-index: 3; opacity: 0; transition: opacity 0.2s; pointer-events: none;
.progress-container:hover .progress-handle opacity: 1;
/* Bottom Controls Row */ .controls-bottom display: flex; justify-content: space-between; align-items: center; gap: 16px;
.controls-left, .controls-right display: flex; align-items: center; gap: 16px;
/* Buttons & Icons */ .control-btn background: transparent; border: none; color: white; cursor: pointer; padding: 6px; display: flex; align-items: center; justify-content: center; border-radius: 4px; transition: all 0.2s;
.control-btn:hover background-color: rgba(255,255,255,0.2); transform: scale(1.05);
.time-display color: white; font-size: 14px; font-weight: 500; letter-spacing: 0.5px;
/* Volume Slider */ .volume-control 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: 5px; outline: none;
.volume-slider::-webkit-slider-thumb -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #ff0000; cursor: pointer;
/* Responsive */ @media (max-width: 768px) .volume-slider width: 50px; .time-display font-size: 12px;
CSS Highlights:
Volume control is handled via an input range element. The HTML5 video API accepts values between 0.0 and 1.0.
function handleVolume()
video.volume = volumeSlider.value;
// Optional: Mute logic if value is 0
volumeSlider.addEventListener('input', handleVolume);
You can see and fork the final version here:
👉 Custom YouTube-style HTML5 Video Player on CodePen To create a YouTube-style HTML5 video player on