October 4, 2018
•Last updated November 5, 2023
Let's Build: With JavaScript - HTML5 Video Player
Welcome to the next installment to my Let's Build: With JavaScript series. This tutorial teaches you how to create, customize, and manipulate an HTML5 video player using vanilla JavaScript.
I'll be using a combination of HTML, CSS, and JavaScript to accomplish this task which is a bit more advanced than the previous installments in this series.
I got the initial idea for manipulating an HTML5 Video element from Wes Bos who did a very similar video tutorial in is JavaScript 30 course. I highly recommend giving his course a go as it teaches you modern tips and tricks for JavaScript workflow.
Much of the credit for the concept and code structure goes to Wes on this one. I wanted to take a stab at doing something similar with my own flair. I used different styles, naming conventions, and more to accomplish what you see on the final CodePen.
Let's get coding:
The very first step to getting a video element on the page is actually rendering one using the newish video
tag.
HTML
My final HTML
looks like the following:
<div class="player">
<video class="player-video" src="https://staging.coverr.co/s3/mp4/Down_by_the_River.mp4"></video>
<div class="player-controls">
<div class="progress">
<div class="filled-progress"></div>
</div>
<div class="ply-btn">
<button class="player-btn toggle-play" title="Toggle Play">
<svg class="" width="16" height="16" viewBox="0 0 16 16"><title>play</title><path d="M3 2l10 6-10 6z"></path></svg>
</button>
</div>
<div class="sliders">
<input type="range" name="volume" class="player-slider" min="0" max="1" step="0.05" value="1">
<input type="range" name="playbackRate" class="player-slider" min="0.5" max="2" step="0.1" value="1">
</div>
<button data-skip="-10" class="player-btn">« 10s</button>
<button data-skip="10" class="player-btn">10s »</button>
</div>
</div>
We need a variety of controls to manipulate certain parts of the video
element's API
. Within each video
element there are a huge amount of properties we can access with JavaScript to manipulate. Everything from volume to playbackRate can be adjusted with a few lines of code.
The HTML
above contains
- a containing
.player
div which we'll use for styling. - a
video
element with asrc
attribute referenced - a
.player-controls
div of which will have all the controls we want - a
.play-btn
div for wrapping the play/pause icons we'll make use of - input range sliders for controlling volume and playback rate speeds
- buttons for skipping ahead and backward
These controls and elements may seem daunting to think about having to manipulate with JavaScript but most of the logic will ultimately deal with user interaction. This means our JavaScript will rely heavily on event listeners to do anything and everything.
CSS
While I called it CSS
above, I'll actually be using SCSS
to write my styles. You'll need a precompiler to write something similar. Check out the codepen for compiled styles if you need to.
$accent-color: #FFEC41;
body {
align-items: center;
background: #000046;
background: linear-gradient(to right, #1CB5E0, #000046);
display: flex;
height: 100vh;
justify-content: center;
margin: 0;
padding: 0;
}
.player {
max-width: 800px;
border: 6px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 0 25px rgba(0, 0, 0, 0.1), 0 3px 3px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
&:hover {
.progress {
height: 10px;
}
.player-controls {
transform: translateY(0);
}
}
}
.player:-webkit-full-screen,
.player:fullscreen {
max-width: none;
width: 100%;
}
.play-btn {
flex: 1;
}
.player-video {
width: 100%;
display: block;
}
.player-btn {
background: none;
border: 0;
color: white;
text-align: center;
max-width: 60px;
padding: 5px 8px;
svg {
fill: #FFFFFF;
}
&:hover,
&:focus {
border-color: $accent-color;
background: rgba(255, 255, 255, .2);
}
}
.player-slider {
width: 10px;
height: 30px;
}
.player-controls {
align-items: center;
display: flex;
position: absolute;
bottom: 0;
width: 100%;
transform: translateY(100%) translateY(-5px);
transition: all 0.3s;
flex-wrap: wrap;
background: rgba(0, 0, 0, 0.3);
}
.player-controls > * {
flex: 1;
}
.progress {
position: relative;
display: flex;
flex: 10;
flex-basis: 100%;
height: 4px;
transition: height 0.3s;
background: rgba(0, 0, 0, 0.5);
}
.filled-progress {
width: 50%;
background: $accent-color;
flex: 0;
flex-basis: 50%;
}
.sliders {
max-width: 200px;
display: flex;
}
input[type=range] {
-webkit-appearance: none;
background: transparent;
width: 100%;
margin: 0 5px;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 8px;
cursor: pointer;
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0), 0 0 1px rgba(13, 13, 13, 0);
background: rgba(255, 255, 255, 0.5);
border-radius: 10px;
border: 0.2px solid rgba(1, 1, 1, 0);
}
input[type=range]::-webkit-slider-thumb {
height: 15px;
width: 15px;
border-radius: 50px;
background: white;
cursor: pointer;
-webkit-appearance: none;
margin-top: -3.5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: rgba(255, 255, 255, 0.8);
}
input[type=range]::-moz-range-track {
width: 100%;
height: 8px;
cursor: pointer;
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0), 0 0 1px rgba(13, 13, 13, 0);
background: #ffffff;
border-radius: 10px;
border: 0.2px solid rgba(1, 1, 1, 0);
}
input[type=range]::-moz-range-thumb {
box-shadow: 0 0 3px rgba(0, 0, 0, 0), 0 0 1px rgba(13, 13, 13, 0);
height: 15px;
width: 15px;
border-radius: 50px;
background: white;
cursor: pointer;
}
A lot of browser default controls for buttons, input ranges, and more are just plain ugly. This code makes up for that as well as sets a max-width on our video to keep it centered on the page. Feel free to go nuts here and write your own styles.
The JavaScript
Our JavaScript will focus on an object-oriented approach. This means that we'll create a lot of little functions that do mostly one thing. Combined these will make the video a fully featured component. Separated, it will make it easier to understand what is going on for any other developer who might see the code. Most of the iteractions on the video itself are invoked by event listeners (listing for user interaction). We'll write functions that do things when specific actions are captured. The final logic is here:
const player = document.querySelector('.player');
const video = player.querySelector('.player-video');
const progress = player.querySelector('.progress');
const progressFilled = player.querySelector('.filled-progress');
const toggle = player.querySelector('.toggle-play');
const skippers = player.querySelectorAll('[data-skip]');
const ranges = player.querySelectorAll('.player-slider');
// Logic
function togglePlay() {
const playState = video.paused ? 'play' : 'pause';
video[playState](); // Call play or paused method
}
function updateButton() {
const togglePlayBtn = document.querySelector('.toggle-play');
if(this.paused) {
togglePlayBtn.innerHTML = `<svg class="" width="16" height="16" viewBox="0 0 16 16"><title>play</title><path d="M3 2l10 6-10 6z"></path></svg>`;
} else {
togglePlayBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16"><title>pause</title><path d="M2 2h5v12H2zm7 0h5v12H9z"></path></svg>`;
}
}
function skip() {
video.currentTime += parseFloat(this.dataset.skip);
}
function rangeUpdate() {
video[this.name] = this.value;
}
function progressUpdate() {
const percent = (video.currentTime / video.duration) * 100;
progressFilled.style.flexBasis = `${percent}%`;
}
function scrub(e) {
const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration;
video.currentTime = scrubTime;
}
// Event listeners
video.addEventListener('click', togglePlay);
video.addEventListener('play', updateButton);
video.addEventListener('pause', updateButton);
video.addEventListener('timeupdate', progressUpdate);
toggle.addEventListener('click', togglePlay);
skippers.forEach(button => button.addEventListener('click', skip));
ranges.forEach(range => range.addEventListener('change', rangeUpdate));
ranges.forEach(range => range.addEventListener('mousemove', rangeUpdate));
let mousedown = false;
progress.addEventListener('click', scrub);
progress.addEventListener('mousemove', (e) => mousedown && scrub(e));
progress.addEventListener('mousedown', () => mousedown = true);
progress.addEventListener('mouseup', () => mousedown = false);
As you can see, the amount of event listeners we need here gets pretty daunting but nevertheless, the whole things work quite well. Be sure to follow along in the video for a complete context. The written versions are for context purposes but also just for reference in case you'd rather reference the code directly rather than codepen.
Thanks so much for watching/reading. There's much more to come. In case you're new here be sure to check out the other videos in this series listed below:
Let's Build: With JavaScript Series
- Let’s Build: With JavaScript – DIY Dropdowns and Responsive Menus
- Let’s Build: With JavaScript – Broadcast Bar with Cookies
- Let’s Build: With JavaScript – Sticky Nav
- Let’s Build: With JavaScript – Dynamic Tabs
- Let’s Build: With JavaScript – Modals
- Let's Build: With JavaScript - HTML5 Video Player
Categories
Collection
Part of the Let's Build: With JavaScript collection
Products and courses
-
Hello Hotwire
A course on Hotwire + Ruby on Rails.
-
Hello Rails
A course for newcomers to Ruby on Rails.
-
Rails UI
UI templates and components for Rails.