tutorial // Jan 27, 2023

How to Dynamically Make Videos Responsive with JavaScript

How to dynamically detect all of the videos in a webpage and apply CSS to them to make them adjust their proportions fluidly to the page.

How to Dynamically Make Videos Responsive with JavaScript

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start:

Terminal

cd app && joystick start

After this, your app should be running and we're ready to get started.

Adding some global CSS

To ensure our demo doesn't look wonky, real quick, we want to open up the /index.css file at the root of the app we just created and change it to look like this:

/index.css

*,
*:before,
*:after {
  box-sizing: border-box;
}

body {
  font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif;
  font-size: 16px;
  background: #fff;
}

Here, we're adding the *, *:before, *:after part at the top. This is selecting all * elements on the page as well as the CSS pseudo-elements of all elements on the page. The style we're applying here—box-sizing: border-box—is telling CSS to render all elements, accounting for their padding as part of their width (this ensures elements don't overflow when their parent container has padding).

That's all we need. Next, let's rig up a component where we'll be able to show off our responsive video solution.

Wiring up a test component

To organize our work, first, we're going to wire up a Joystick component that will contain our example HTML with some videos, the JavaScript for wrapping our videos, and the CSS to bring it all together.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  render: () => {
    return `
      <div class="container">
        <h1>Test Video #1</h1>
        <iframe width="560" height="315" src="https://www.youtube.com/embed/gyvZhf3za0Y" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
        <h1>Test Video #2</h1>
        <iframe width="560" height="315" src="https://www.youtube.com/embed/Gh8eadbpgJg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
        <h1>Test Video #3</h1>
        <iframe width="560" height="315" src="https://www.youtube.com/embed/xQaPVjKp1LE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
      </div>
    `;
  },
});

export default Index;

Here, we're replacing the existing contents of /ui/pages/index/index.js with a simpler component that renders some HTML containing our test videos. For our example we're using YouTube <iframe /> embeds, but as you'll see later, this solution will work with <video> and <embed> tags, too.

If you're curious, behind the scenes this component is being rendered by Joystick via the /index.server.js file at the root of the project. Inside, we start up the server for our app, passing any routes to define. The page/route associated with the component above—/ or "index"—is automatically wired up for us as part of a new Joystick app.

Next, let's wire up and walk through the JavaScript necessary to "detect" our videos.

Adding the JavaScript to automatically wrap videos

This is the hardest part of this tutorial. What we want to do is tell JavaScript to automatically "find" all video-related elements on the page and wrap them in a <div>. That <div> will receive a special CSS class that we'll use to apply the styling necessary to make our video responsive.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  lifecycle: {
    onMount: (component = {}) => {
      const videoTags = component.DOMNode.querySelectorAll("iframe, video, embed");
      videoTags.forEach((videoTag) => {
        const responsiveWrapper = document.createElement("div");
        responsiveWrapper.classList.add("responsive-video");
        responsiveWrapper.innerHTML = videoTag.outerHTML;
        videoTag.parentNode.replaceChild(responsiveWrapper, videoTag);
      });
    },
  },
  render: () => {
    return `
      <div class="container">
        <h1>Test Video #1</h1>
        <iframe width="560" height="315" src="https://www.youtube.com/embed/gyvZhf3za0Y" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
        ...
      </div>
    `;
  },
});

export default Index;

First, we've added a new property to the object we're passing to our ui.component() definition: lifecycle. This object receives custom definitions for the various "lifecycle methods" that Joystick makes available. These methods, if defined, are automatically called by Joystick behind the scenes relative to the "step" in the lifecycle they correspond to.

Here, we're defining the onMount method which is called by Joystick when our component "mounts" or has its HTML rendered in to the DOM (Document Object Model) of the browser (i.e., "on screen").

To this method, we expect Joystick to pass us the component instance which is an object containing the current in-memory representation of our component. To start, here, we're accessing the component.DOMNode property which gives us access to the rendered DOM node for our component in the browser. Think of this like a pointer to say "go get the box that represents our component on screen."

On this, we call the querySelectorAll() method, passing a string containing the element selectors we want to get. Here, we use a comma-separated list to say we want to get all instances of these three elements in this component: iframe, video, and embed. This is important. Notice that we're saying querySelectorAll() and not the more common querySelector(). The latter would stop at the first match and return only that whereas the above gets all matching elements.

In return, we expect videoTags to contain a DOM NodeList.

It's important to note: though it looks like an array, a NodeList is slightly different in its makeup. While this won't impact our code here, it's important to note that certain array methods (e.g., .map() and .filter()) are not available on a NodeList. To access these, you'd need to convert your NodeList to an array using something like Array.from(videoTags) or [...videoTags] first.

With that list, we're going to call the .forEach() method on it to iterate over each videoTag we found. For each tag, first, we want to create the <div> element we're going to wrap the tag with in memory using documentCreateElement. This creates a DOM Node but does not render it to the screen.

Next, we want to modify that <div> to assign a class responsive-video to it and then, set its innerHTML to the .outerHTML of the current videoTag we're looping over. "Outer" here means the entire element, not just its contents. The end result will look like this:

<div class="responsive-video">
  <iframe ... />
</div>

Finally, to render this "wrapped" videoTag on screen, we need to get that videoTag's current parentNode with videoTag.parentNode and then tell the parent to replace the child with our new responsiveWrapper element.

That takes care of the hard part. While our videos won't be "responsive" yet, if you load this up in the browser and inspect the HTML, you should see all of the <iframe /> tags being replaced with our wrapped version.

To finish up, let's take a look at the CSS that will actually make our videos responsive.

Adding the CSS to make videos responsive

This part is fairly straightforward, but does include some light brain teasers in order to understand how it all fits together.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  css: `
    .container {
      max-width: 1000px;
      margin: 0 auto;
      width: 100%;
      padding: 20px;
    }
    
    .responsive-video {
      position: relative;
      overflow: hidden;
      padding-top: 56.25%;
    }

    .responsive-video iframe,
    .responsive-video video,
    .responsive-video embed {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      border: 0;
    }
  `,
  lifecycle: {
    onMount: (component = {}) => {
      const videoTags = component.DOMNode.querySelectorAll("iframe, video, embed");
      videoTags.forEach((videoTag) => { ... });
    },
  },
  render: () => {
    return `
      <div class="container">
        <h1>Test Video #1</h1>
        <iframe width="560" height="315" src="https://www.youtube.com/embed/gyvZhf3za0Y" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
        ...
      </div>
    `;
  },
});

export default Index;

First, to make our video not take up the entire browser, here, we're adding some styles to the .container class wrapping our example HTML. This is just centering the <div> and fixing its width to a max of 1000px. This isn't required for our solution but will make our examples look more like you'd expect in context.

For our actual video, next, we want to work backwards up from the bottom. Like we hinted at with our JavaScript earlier, we want our solution to apply to all iframe, video, and embed elements. Here, we're creating a multi-element CSS selector by finding all of those elements inside of the .responsive-video element, chaining them together with a comma. This will apply the styles we're defining to all elements matching this selector.

Here, we're setting all of these elements to be position: absolute; anchoring their positioning to the top-left corner of the .responsive-video element wrapping them. We also set their width and height to be 100% of their parent.

On the surface, this looks like what we'd expect is for each element to automatically expand their width and height to be 100% of their own width and height. Instead, we're going to rely on the parent to "create the height" for us.

.responsive-video {
  position: relative;
  padding-top: 56.25%;
}

Because our video elements are position absolute, by default, our wrapper .responsive-video element will collapse (i.e., it will be invisible on the page). This is because elements with position: absolute don't take up any height themselves. The trick, then, is to make .responsive-video "create" the height necessary to reveal our video.

To do it, first we need to set .responsive-video to position: relative. This creates an invisible boundary that forces our video elements to position themselves absolutely to .responsive-video (without this, they'd automatically position themselves relative to the next-closest parent with position: relative, or ultimately, the <body>).

Finally, we add padding-top: 56.25%. This is the "secret sauce" to this solution. Though it may seem counter-intuitive, when percentage-based, the padding on this element will adjust itself relative to its parent's width. Even though we're setting a top or vertically-oriented padding, the actual calculation being done in the browser is 56.25% of our .container <div>'s width.

Why 56.25%? Well, most videos—or more specifically, any embed of a video—has an aspect ratio of 16:9. 56.25 is the number we get dividing 9 / 16 and multiplying it by 100 to get the percentage. In other words, because this percentage matches the aspect ratio of the video, whatever pixel amount it creates relative to the current container/parent width will automatically match the proper proportions for our video element.

Remember how earlier we set that <div class="container"> to have a max width of 1000px? Here, assuming we're on a screen that reveals that full width, we'd expect our videos to be 562.5px wide. Because our videos are set to a width 100% and height 100%, as we resize our browser, the width of .container changes, forcing the padding-top to change, too and the videos adapt to fill the full available width and height.

That's it! If we open up a browser and adjust the width, we'll see our videos adapt responsively.

Wrapping up

In this tutorial, we learned how to automatically detect videos in our HTML and dynamically wrap them with a special <div> tag which allowed us to apply styles for making them responsive. Using CSS, we learned how to leverage a percentage-based vertical top-padding on a parent element to make the width of its children adapt automatically.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode