Watching and sharing video content is one of the most common uses of the web,
and the way video is embedded on a webpage has evolved over the years.
Nowadays, adding video files to a webpage can be achieved using the
element which works in all modern browsers and supports a variety of video
The main caveat when using
<video> is that the rendered video player will vary
depending on the browser which is not ideal if you want to provide a consistent
user experience. This is why it is useful to build your own interface that
features custom controls instead of using the browser defaults.
The player we’ll be building in this tutorial will look a lot like the one that is found on YouTube because I think it’s a good idea to replicate some of the functionality that is in something most people are already familiar with.
We won’t be implementing all the features found in the YouTube player as that will make for a longer, more complicated tutorial. However, once you’ve completed this tutorial, I’m confident that you will be able to plug in any new functionality with ease.
You can view a live demo of what we’ll be building, or check out the source code on GitHub.
I’ve prepared the starter
this tutorial on GitHub. You need to clone
it to your
machine and open the project directory in your text editor. You will find the
markup and styles for the player in the
respectively, as well as the video file that we’ll be testing the player with.
index.js file is where we’ll add all the code necessary for the
player to work.
npm install in the terminal to install
browser-sync as a development
dependency for starting a web server and automatically refreshing the page
when any of the files change, followed by
npm start to open up the project in
your browser on http://localhost:3000.
What has been done so far
At the moment, the video player retains the native browser controls which works
just as you’d expect. The markup for the custom controls have already been
defined in the
#video-controls element but they’re hidden by default.
Even though we’re going to implement a custom interface for the controls, it’s a
good idea to retain the
controls attribute on the
<video> element so that
to the browser’s native video controls. For everyone else, the native controls
can be hidden easily and replaced with custom controls as will be demonstrated
A poster image has also been added to the video, and the
preload attribute is
metadata which instructs the browser to fetch only the video metadata
(such as duration). To keep things simple, only one source file in the MP4 has been included for the video in MP4 format
because it’s supported by all major browser and is a pretty safe default. See
for more information on video formats and browser compatibility.
Hide the native controls
The first thing we need to do is to hide the default video controls and provide
our own interface once it is determined that the browser supports HTML5 video. Enter the following code snippet into your
index.js file to make that happen:
canPlayType property is how we are able to detect support for a video
format in the browser. To use it, we need to create an instance of the
<video> element and check if it supports the
canPlayType method. If it does,
it is safe to assume that HTML5 video is supported so the default controls are
promptly disabled in favour of our custom controls.
Toggle the playback state
Let’s start with the basics. You should be able to play and pause the video by
clicking on the play button and the icon should change to match the state of the
video. We’ll start by selecting the video and the play button at the top of
index.js as follows:
Then we’ll create a function that toggles the playback state of the video:
Finally, create an event listener that executes the
togglePlay function when
playButton is clicked:
Easy enough right? Test it out by clicking the play button in your browser. It should play and pause the video appropriately.
This actually sets the tone for the rest of the tutorial. We’ll usually select one of the video controls, create a function that implements a specific functionality, then hook it up via an event listener.
Let’s move on to update the play icon depending on the state of the video. This
is the HTML for the
We have both the play and pause icons in the
<svg> element, but only one is
dispyed at a time while the other is hidden. What we need to do now is toggle
the hidden class on each icon so that the appropriate icon is shown depending on
the state of the video.
First, select the icons at the top of
Next, create the function to update the play button under
And finally, add the following event listeners at the bottom:
When the video is played or paused, the
updatePlayButton function is executed
which toggles the
hidden class on each button. Since we have the
class on the pause icon by default, once the video is played, this icon will be
displayed and the play icon will be hidden. If the video is paused again, the
reverse occurs. You can test this in your browser.
One extra thing to do is updating the text in the tooltip that appears when you
hover over the play button. It reads
play (k) by default, but we need to
update it so it reads
pause (k) when the video is playing.
k is the keyboard
shortcut that we’ll add to play or pause a video later in the tutorial.
updatePlayButton as shown below:
That should set the appropriate text in the tooltip once you hover on the button when the video is playing or paused.
If you’re wondering how the tooltip is shown, here’s the relevant CSS:
Show the video duration and time elapsed
It’s necessary to display the length of a video as that’s one of the first things that users would like to see, so we’ll do that next.
Here is the markup for the duration and time elapsed:
Select both controls in your
index.js file as follows:
We’ll show the total duration of the video once the page loads using the video’s
duration property. This property represents the number of seconds of the
video so we need to convert this number to minutes and seconds before
we can display it. Create a
formatTime function that takes in the
time in seconds and converts it to minutes and seconds:
Next, create an
initializeVideo function below
As shown above, the video’s duration in seconds is rounded to the nearest
integer, formatted to minutes and seconds, and updated on the screen. The
datetime attribute is also updated to a time string that represents the
duration of the video.
Next, let’s hook up the
initializeVideo function to the video’s
loadedmetadata event as shown below. This will cause the video duration to be
updated when the video’s metadata has been loaded.
Following that, let’s update the time elapsed when the video is being played. Here’s the function that helps us achieve what we want:
The event we need to listen for on the video is the
timeupdate event. This
event is fired whenever the playback time indicated by the video’s
property is updated.
The above code ensures that once the video’s
currentTime is updated by virtue
of playing the video, the elapsed time is also updated appropriately.
Update the progress bar
The next thing we’ll do is to update the progress bar as the video is being played. Here’s the markup for the progress bar:
Here, we have the
progress element which is apt for displaying the progress of
any task, while the range input will allow us to scrub through the video quickly
and seamlessly. I’ve styled both elements so that they’re the same width and
height, and the range input is made transparent (except for the thumb which is
the same colour as the value inside the progress bar).
If you’re curious, you can dig into the CSS to find out how I did it. It’s kind of a hack to make the progress bar look like it’s a singular element, but I feel it’s justified for our use case.
min attribute on both is set to zero, and the
value attribute indicates
the current value of both elements. They also need a
max attribute which will
be set to the duration of the video in seconds which is from
already demonstrated above. We can do this in the
but we need to select the elements first:
initalizeVideo as shown below:
Now, the range for both the progress element and range input is between zero and
the video duration in seconds as indicated by the
max attributes on
both elements. This allows us to easily sync the progress bar with the range
input at any point in time as you’ll see.
Let’s go ahead and update the values of the aforementioned elements when the
video is being played so that the progress bar becomes functional. Create a new
updateProgress function below:
Then add a new
timeupdate event listener on the
video element below the first one:
Refresh your browser and try it out. You should see the progress bar update as the video is being played.
Most video players allow you to click on the progress bar to jump to a particular point in the video, and ours is not going to be any different. First, we need to select the tooltip element:
Then add a function to display the timestamp in the tooltip element when the cursor is over the progress bar:
This function uses the position of the cursor on the
seek element to roughly
work out where in the range input the user is hovering on, and stores the
position in a
data-seek attribute while updating the tooltip to reflect the
timestamp at that position.
Hook up the
updateSeekTooltip function to the
mousemove event on the
control to see it the effect in action:
Once the value of
seek element is changed either by clicking or dragging
the thumb, we want the video to jump to the time set in the
attribute. Create a new
skipAhead function below
This function will be executed when the value of the
seek element changes can
be monitored using the
input event. We then get the value of the
attribute and check if it exists. If it does, we grab the value and update the
video’s elapsed time and the progress bar to that value. If the
property does not exist (on mobile for example), the value of the
element is used instead.
This creates the effect of jumping ahead to a different position in the video.
In the above snippet, you can find markup for all the volume related controls. We have a button that represents the volume icon depending on the state of the video’s volume, and a range input that controls the volume of the video.
The first thing we need to do is update the volume of the video when the value
#volume element changes. We also need to update the icon to reflect the
current volume of the video.
As you can see, volume input ranges from 0 to 1, and each step in the input increases the volume by 0.01. It is set this way to make it consistent with the video’s volume property which also ranges from 0 to 1 where 0 is the lowest volume and 1 is the highest.
Go ahead and select the button, icons and input in your
Next, create a new
updateVolume function to update the volume as soon as the
volume input is changed:
And hook it up to the
volume element as follows:
At this point, you will notice the volume decrease when you slide the range to the left, and vice versa. We need to add another function to update the icon whenever the volume changes:
When this function executed, all the icons are hidden then one of them is displayed depending on which condition evaluates to true.
We can run
updateVolumeIcon each time the volume changes by listening for the
volumechange event as follows:
This is what you should get in your browser after making this change:
One more thing we need to add is the ability to mute and unmute the video by
clicking on the volume icon. We’ll create a new
toggleMute function for this
Then run the function when the
volumeButton is clicked:
This function toggles the state of the
muted property on the video to true or
false. When the video is muted, the volume is stored in a
attribute on the
volume element, so that when the video is unmuted, we can
restore the state of the volume to its previous value.
Here’s how that looks in practice:
Click the video to play or pause it
In many video player applications, clicking on the video itself is often a quicker way to play or pause the video so let’s make that possible in our player.
All we need to do is add listen for the
click event on the
video and run the
togglePlay function when the event fires:
While this works, let’s make it more interesting by adding a bit of feedback when you play or pause a video by clicking on it just like the way it’s done on YouTube or Netflix.
Here’s the HTML for our animation:
And here’s the relevant CSS:
By default, the
.playback-animation element is made completely transparent
opacity property. To replicate the animation from YouTube, we’ll
make use of the Web Animation
animate the opacity and scale of this element.
Select it first at the top of
Then create the following function below the all other functions in
animate method takes in an array of keyframe objects and an options object where you can control the duration of the animation amongst other things.
Now, add a second the
click event listener on the
The effect is that you now see a short animation when you play or pause the video by clicking on it.
Next, let’s make the full-screen button functional. To make the video
full-screen (including the controls) we have to select the
element and ask the browser to place it (and its descendants) in full-screen.
Select both the button and video container in your
Then create a new
And add a
click event listener on the
fullScreenButton as shown below:
toggleFullScreen function checks if the document is in full-screen mode
first, if so it exits back to window mode. Otherwise, it places the
videoContainer element in full-screen.
One more thing we need to do in this section is update the full-screen icon and the text in the tooltip that appears when you hover on the button. First, select the icons:
Then create a function to update the button when the
videoContainer goes in and
out of full-screen mode:
updateFullscreenButton to the
handler on the
And it works as expected! Test it in your browser or see the GIF below.
Add Picture-In-Picture support
The Picture-in-Picture (PiP) API allows users to watch videos in a floating window (always on top of other windows) so they can keep an eye on what they’re watching while interacting with other sites, or applications.
At this point in time, this API is only supported in a handful browsers so we have to use feature detection to hide the PiP button in browsers that do not support it so that users don’t see a button that they can’t use.
Here’s the code that helps us achieve that. Add it below the other event listeners.
As we have been doing throughout this tutorial, we need to select the relevant control first:
Then create the function that toggles Picture-in-Picture mode:
I’ve made the
togglePip function asynchronous so that we can catch errors in
requestPictureInPicture() method rejects, which can happen for a
number of reasons. In a real word app, you may want to show an error message to
the user instead of logging it to the console.
Next, listen for the
click event on the
pipButton and add the
function as the handler for the event.
Now, clicking the
pipButton should enter or exit Picture-in-Picture mode. You
can also close the PiP window by clicking on the close button in the top right.
Toggle the video controls
The video controls take up some space and blocks the user’s view of some of the content. It is better to hide them when they are not in use, and show them again when hovering over the video.
Write the two functions below for this purpose:
What we want to do here is hide the controls when the cursor leaves the video
interface. But we want to make sure the controls are always shown when the video
is paused, hence the conditional in
To achieve this, we’ll use the
onmouseleave event handlers
on both the
video element and the
videoControls as shown below:
Add keyboard shortcuts
The last feature we’ll add to our player is the ability to use the keyboard to control the video playback. It’s really just a matter of running the functions we’ve already written when a specific key is pressed. The shortcuts we’ll implement here are as follows:
k: Play or pause the video
m: Mute or unmute the video
f: Toggle fullscreen
p: Toggle Picture-in-Picture mode
What we’ll do here is listen for the
keyup event on the document, detect the
key that was pressed and run the relevant functions for key.
A switch statement is used above to detect which key was pressed and then
execute the relevant code. The reason
hideControls is called after two seconds
is to imitate the behaviour on YouTube where, when using the keyboard shortcut
to play the video, the controls do not hide immediately once the video starts
playing but do so after a short delay.
There are so many ways to improve the video player but the tutorial was already getting too long so I had to stop here. If you’re interested in extending the functionality of the player, here are some ideas:
- Add support for captions and subtitles.
- Add speed support.
- Add the ability to fast-forward or rewind the video.
- Add ability to choose video resolution (720p, 480p, 360p, 240p).
I hope this tutorial was helpful to you. If you have any questions, please leave a comment below and I’ll get back to you. Don’t forget to checkout the full source code on GitHub.
Thanks for reading, and happy coding!