Make your embedded videos responsive with CSS or JS

Published in 28-08-2016 by Luis Lopez

When you insert an embedded video from Youtube, Vimeo, Dailymotion or any other video site, you’re asked to insert a certain HTML code inside your website (a DIV container) but the problem comes when your container for the video doesn’t have a fixed constant width; with the importance that responsive web design has drastically gained over the past years, it’s imperative that you adapt the videos you insert on your site, blog or app to the container size (width) because most of the times, these embedded objects, whether it’s an IFRAME (YouTube does this), an HTML5 video tag or an object one, they often come with fixed values (as HTML attributes) for the height and the width of said video.

The strategies that have been arising lately have been a few but the most notorious ones are:

  1. An old trick mentioned in [this article] by CSSTricks and discovered in 2009 that consists of wrapping Iframes in a container that was positioned as “absolute”, relative to an outer wrapper div that relied on a percentage based padding-bottom value for the height. This trick then came to become a famous jQuery plugin called Fitvids.js (made by Chris Coyer of CSSTricks).

  2. Grabbing every Iframe (only those with a video embed) and object HTML tag and determining its aspect ratio (it’s often preferred to use the 16:9 widescreen ratio), then making the width equal to its container’s width and then setting the height equal to a new height which is calculated with the aspect ratio. But what if the user resizes the page? We can create a “window resize” event listener that triggers a function when the viewport size changes; after that, all we have to do is encapsulate the resizing algorithm inside a throttled function (to lessen the operations per second of the event listener triggered function) and adding it to the event listener.

If you wonder why I only mentioned Iframes but not HTML5 video tags was because HTML5 video tags happen to be able to have a width: 100% !important CSS property.

Using CSS (Best Method)

It’s no discussion that a pure CSS solution that works across all browsers (old and modern) is going to win over the other methods. The CSS method is the one I mentioned above, it consists of nothing more than two wrapping divs; you can use those divs in your production environment and place the embedded video code inside it. But what if you find this cumbersome to do with every video? That’s when JavaScript comes into play if you can’t afford yourself to wrap your videos with container divs every time you insert one.

This is what CSSTrick proposes for Iframes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.videoWrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
padding-top: 25px;
height: 0;
}
.videoWrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}

And for object and embed tags, you can modify the second CSS block to look like this:

1
2
3
4
5
6
7
8
9
.videoWrapper iframe,
.videoWrapper embed,
.videoWrapper object {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}

This way you will force those three HTML tags to take all the size of the video wrapper box (hence the 100% size values). Now, to make use of this class all you need to do is insert an embedded video code inside a div with the class of videoWrapper.

1
2
3
<div class="videoWrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/57ThMAIE_Lw" frameborder="0" allowfullscreen></iframe>
</div>

Someone on Reddit also said that Bootstrap has a helper class for this, you may want to check that out.

Which method to use

I’m going to teach you the 2nd method with pure JavaScript, no jQuery (although you can use it, it will save some lines of code) but making sure it adds support for older browsers by adding some polyfills and custom functions.

VERY IMPORTANT: The vanilla JavaScript version can be short if you want to support IE9+ but if you absolutely need to support IE6 to IE8, I have bad news, you need 3 brutal polyfills for it to work so, if you want the jQuery version (but not the plugin) click here to take you to the part I placed the jQuery code snippet.

When it comes to a production environment and if you are serving jQuery (which is probably cached in most computers nowadays) you might as well add the Fitvids.js plugin and use it, you can read how to use Fitvids.js here. You can also use the following CDN link to hotlink it from your project instead of saving the JavaScript file on your server.

1
<script src="https://cdnjs.cloudflare.com/ajax/libs/fitvids/1.1.0/jquery.fitvids.min.js"></script>

Doing it with vanilla JavaScript (your own Fitvids.js) can be a nightmare, I tell you. I actually tried and succeed but not without adding a bunch of polyfills and abstracting a lot of procedures into small functions. Instead, I invite you to read the source code of Fitvids.js because it’s really not that large and the author did a good job with the abstractions (so it’s easily readable).

Let’s write the 2nd method

We’re going to start by adding very important polyfills. In layman’s terms, this piece of code will support certain browsers for certain features that they may not have available.

In order for Event Listeners to work in IE8 or lower, you need the following polyfill, it came straight from the creator of jQuery. In your JavaScript file (assuming you’re targeting one from your HTML files) add the following code at the very top:

1
2
3
4
5
6
7
8
9
10
11
function addEvent(obj, type, fn) {
if (obj.attachEvent) {
obj['e'+type+fn] = fn;
obj[type+fn] = function() {
obj['e'+type+fn](window.event);
};
obj.attachEvent('on'+type, obj[type+fn]);
} else {
obj.addEventListener(type, fn, false);
}
}

But wait, there’s more! replacing jQuery comes with some pros and cons, and one of the cons is having to add this monstrous polyfill that I took from StackOverflow just to support the DOMContentLoaded (equivalent to $(document).ready) in order to let the document be parsed first before executing the JS code; you can opt out from pasting this one in your code if you’d like to tell IE6, IE7 and IE8 to f*ck off:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
(function(funcName, baseObj) {
funcName = funcName || "docReady";
baseObj = baseObj || window;
var readyList = [];
var readyFired = false;
var readyEventHandlersInstalled = false;
function ready() {
if (!readyFired) {
readyFired = true;
for (var i = 0; i < readyList.length; i++) {
readyList[i].fn.call(window, readyList[i].ctx);
}
readyList = [];
}
}
function readyStateChange() {
if ( document.readyState === "complete" ) {
ready();
}
}
baseObj[funcName] = function(callback, context) {
if (readyFired) {
setTimeout(function() {callback(context);}, 1);
return;
} else {
readyList.push({fn: callback, ctx: context});
}
if (document.readyState === "complete") {
setTimeout(ready, 1);
} else if (!readyEventHandlersInstalled) {
if (document.addEventListener) {
document.addEventListener("DOMContentLoaded", ready, false);
window.addEventListener("load", ready, false);
} else {
document.attachEvent("onreadystatechange", readyStateChange);
window.attachEvent("onload", ready);
}
readyEventHandlersInstalled = true;
}
}
})("docReady", window);

This can be avoided if you have your final javascript file at the very bottom of the body HTML tag.

Helper array iterative methods

You may be tempted to use Array.prototype.map, filter and forEach as the native ES5 implementations but you gotta know that because these methods were added from ES5 (JavaScript spec, released in 2009) onwards, older browsers may not support them; what we need to do is either add a library like Lodash or UnderscoreJS, or create our own array iterative methods. Let’s add the ones we need which are forEach and filter.

Below the polyfill we just added, insert the following functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function _each(items, action) {
for (var i = 0, len = items.length; i < len; i++) {
action(items[i], i);
}
}
function _filter(items, test) {
var filtered = [];
for (var i = 0; i < items.length; i++) {
var item = items[i];
if (test(item, i)) filtered.push(item);
}
return filtered;
}

I won’t explain much of what this code does because you may have already used those functions but to summarize it all, _each will iterate over an array (in this case, an array of DOM elements) and _filter takes an array and returns a new one filled with elements from the original array that pass a certain test or condition.

The video resizing algorithm

This algorithm is kinda simple, to be honest, all it does is resize the DOM element corresponding to the video object to be resized by using the container element’s width and an aspect ratio that can be either pre-defined as a constant or obtained dynamically using the video’s width and height HTML properties. I chose to use a pre-defined aspect ratio, the 16:9 ratio is my favorite but you can choose to do what you think is correct. Add the following function below the array iterative method functions:

1
2
3
4
5
6
7
function resize(video, newWidth) {
var ASPECT_RATIO = 9/16; // 16:9
var newHeight = (ASPECT_RATIO * newWidth);
video.setAttribute('width', newWidth.toString());
video.setAttribute('height', newHeight.toString());
}

Very simple, huh? All this does is change the width of the video to the current width of the containing DOM element and the height of the video to the width multiplied by the aspect ratio’s factor.

Debouncing the resizing function

In the following section, we’re going to create a function that resizes every video on the page, but as it’s explained in that section, you should probably prevent it from executing very quickly; remember that the resize event might trigger the callback function in small intervals because even if one single pixel changes, it will trigger the callback.

We need to add a debouncer, which will create a new function based on one that’s been passed to it and will make sure it doesn’t execute again until a certain amount of time has passed (in milliseconds); I’ve taken the liberty of using David Walsh’s debounce snippet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function _debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}

If the debounce function doesn’t work in IE6 up to IE8, find another alternative or remove the arguments object and its implementation inside this snippet since we’re not going to need arguments for this function.

Adding event listeners

This is almost the final part, we need to let JavaScript know when the document has been parsed (equivalent to jQuery’s ready method) so we can start selecting some DOM elements. It’s time to start by adding an event listener for the parsed document and inside, grab every Iframe that is an embedded video (you can also add object tags, it’s up to you).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
docReady(function() {
var iframes = document.getElementsByTagName('iframe');
var isVideo = /(youtube)|(vimeo)/i;
var videos = _filter(iframes, function(iframe) {
return isVideo.test(iframe.getAttribute('src'));
});
var resizeVideos = function() {
_each(videos, function(video) {
var newWidth = video.parentElement.offsetWidth;
_resize(video, newWidth);
});
};
resizeVideos();
addEvent(window, 'resize', _debounce(resizeVideos, 70));
});

We start by selecting our Iframes and initialize a regular expression to detect YouTube or Vimeo videos (add more if you want or need) and then, the videos variable will be an array of Iframes that include the word “youtube” or “vimeo” in the src attribute. The reason I included a function inside the docReady callback was because I wanted it to have access to the videos variable and possibly, if you wanted a certain element’s width as the containing element instead of the parent element of the video, you can set up a variable that selects the DOM element and then I use its offsetWidth instead of video.parentElement.offsetWidth. If you don’t know what I mean, take this blog again as an example, since I will only add videos to the .post div (only the first one it finds) and not somewhere else, I can replace the value of newWidth with document.getElementsByClassName('post')[0].

Note: the document.getElementsByClassName method needs a polyfill if you’re adding support for older browsers or versions of IE. The polyfill can be found at Mozilla Developer Network (I think) or this Gist will all credit due to the author Eike Send.

The function that gets declared inside the scope of the callback (docReady) iterates over the videos variable and resizes each of those videos. You can play around with scope and context but this is the way I chose to do it; doesn’t mean you can’t modify it. Actually… it’s not a bad idea to take the resizeVideos function outside and place the three first statements inside that function at its top for it to have access to said DOM elements.

After that, we call that very function and add an event listener so that when the viewport’s width changes, the function resizeVideos gets called, but, notice the _debounce wrapper? It’s returning a function that can only be called once 70 milliseconds have passed since the last execution; this is useful because a resize event is very CPU intensive and throttling it will prevent it from executing every time a single pixel changes during the window resizing event.

jQuery Version

If you are sick of polyfills and long blocks of code, you can use this jQuery version that I prepared. It’s the same code but shorter because of the obvious reasons; it’s also compatible with all browsers that jQuery supports and can be exported into its own micro-plugin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Here goes the _debounce function
function resizeVideos() {
var ASPECT_RATIO = 9/16; // 16:9
var isVideo = /(youtube)|(vimeo)/i;
$("iframe").filter(function() {
return isVideo.test($(this).attr('src'));
}).each(function() {
var newWidth = $(this).parent().width();
var newHeight = (ASPECT_RATIO * newWidth);
$(this).attr('width', newWidth.toString());
$(this).attr('height', newHeight.toString());
});
}
$(document).ready(function() {
resizeVideos();
$(window).resize(_debounce(resizeVideos, 70));
});

If you are looking for the debounce function, go upside a bit and you’ll find it.

Conclusion

Doing this in vanilla JavaScript isn’t that hard if you’re not adding support for IE8 or below (and some other browsers that may have some compatibility issues with ES5 and certain DOM manipulation methods). Once browser support comes into play, adding polyfills can be daunting and using jQuery may be tempting (and absolutely necessary). If you found a bug or efficiency mistake in my code please feel free to point it out!


Comments: