Build a responsive media browser with CSS

Categories

Using the power of modern CSS layout, we create a flexible media browser and video player layout that maintains its aspect ratio at all viewports.


Something that has been a bit of a brain teaser historically, is aspect ratio—specifically, maintaining it—especially over the last 10 or so years, since responsive design has been the industry standard way of building websites.

A context that has always been particularly problematic for this layout brain-teaser is video—especially a third-party embedded video, such as YouTube.

In this tutorial we are going to solve all of these problems using the magic of Flexbox. What we will end up with at the end is a water-tight layout that can be adapted for many use-cases where aspect ratio needs to be maintained, while other elements put that at risk.

See a completed demo

Getting started permalink

Before we start, let’s get you up and running with some starter files. I’ve set us up a project that uses Eleventy to generate a nice collection of pages that all run off the same template. This will make the media browser fully interactive, using good ol’ HTML and URLs.

To get started:

  1. Download these starter files
  2. Extract the contents
  3. Open your terminal inside the folder and run npm install
  4. Serve up and watch for file changes with npm start

When you run npm start and open your browser at <http://localhost:8080>, you should see something that looks a bit like this:

A title, video and collection of links in un-styled HTML

A quick look at the markup permalink

All the HTML for this tutorial is already in place to keep things focused on CSS, but let’s look at the main .media-browser element’s markup.

You don’t need to copy this code

Code language
html
<div class="media-browser">
  <a class="skip-link" href="#video-nav">Skip to the video navigation</a>
  <div class="media-browser__video">
    <div class="video-player">
      <iframe
        src="https://player.vimeo.com/video/4753710?color=666&title=0&byline=0&portrait=0&autoplay=1"
        width="640"
        height="360"
        frameborder="0"
        allow="autoplay; fullscreen"
        allowfullscreen
      ></iframe>
    </div>
  </div>
  <nav class="media-browser__nav" aria-label="Videos" id="video-nav" tabindex="-1">
    <ol class="media-browser__links">
      <li><a href="#">Albatross Soup</a></li>
      <li><a href="#">All Cats Are Gray in the Dark</a></li>
      <li><a href="#" aria-current="page">Thrasher - Nick Mullins Sponsor Me</a></li>
      <li><a href="#">Thrasher Bust or Bail 2008</a></li>
      <li><a href="#">‘Aliens’ (2020)</a></li>
      <li><a href="#">A Normal Day of Sassi (쎄씨의 하루)</a></li>
      <li><a href="#">Battle Patrol 2084</a></li>
    </ol>
  </nav>
</div>

What we have here is an element that features a Vimeo embed and a collection of links which link to other pages, which contain the same media browser, with a different video loaded. At the start of the .media-browser element, we have a skip link that allows the user to skip straight to the <nav> element to move to other videos.

We make the <nav> element programatically focusable by setting tabindex="-1", which helps to ensure that when a user activates the skip link, the next focusable element will be the first link to another video.

Another useful addition to this <nav> is that we have an aria-label. This is actually best practice if you have more than one <nav> landmark so assistive technology, such as a screen reader, will more helpfully announce them.

That’ll do for HTML programming for now. We’ll cover stuff in more detail as we style it.

Global styles permalink

The CSS in this tutorial uses some of the principles from CUBE CSS.

With that in mind, the first thing we will do is add some global CSS. In your starter files, open src/css/global.css and add the following to it:

Code language
css
body {
  padding: 2.5rem 1.5rem;
  font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  line-height: 1.5;
  background: #ffffff;
  color: #333333;
}

main {
  max-width: 50rem;
  margin: 0 auto;
}

main > * + * {
  margin-top: 2em;
}

h1 {
  font-weight: 500;
}

p {
  max-width: 60ch;
}

a {
  color: currentColor;
}

:focus {
  outline: 1px dashed;
  outline-offset: 0.25rem;
}

This is all fairly straightforward, global styling. We’re setting some sensible defaults, such as a system font stack, link styles and importantly, global focus styles.

We’re also adding some flow and rhythm by telling all direct siblings of the <main> element to add 2em of top margin. We do this using a lobotomised owl selector.

Your version of the project should look a bit like this now:

The same view as before, but looking much smarter with sans serif fonts and limited widths

Responsive video player permalink

Now we have our global styles, let’s focus on some specific elements. Before we tackle the .media-browser, let’s build out the .video-player. Still inside global.css, add the following:

Code language
css
/* Video player */
.video-player {
  position: relative;
  padding-bottom: 56.25%;
}

.video-player > * {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

What we do here is create a container with a 16:9 aspect ratio. The 56.25% might look like a magic number, but it’s actually a result of the following calculation:

  1. Divide 9 by 16: 0.5625
  2. Multiply that by 100 (or move decimal 2 places): 56.25

This works with any aspect ratio!

We add padding to the bottom of the .video-player because vertical padding percentages work from the element’s width. Let’s say that our video player happens to find itself in a 1000px wide container: it would have a bottom padding value of 562.5px.

We then target the direct child of the video player and make it absolutely positioned. Because we pull the video itself out of the document flow, it fits all sides by setting width and height, then sticking it in the top left with positioning. This means that the video element will always maintain the same proportions as its responsive parent, thanks to its padding. Handy, right?

Our skip link needs to be visually hidden unless it is focused. We also want to make that absolutely positioned, so it doesn’t affect our .media-browser layout.

While you’re still in global.css, add the following:

Code language
css
.skip-link {
  display: inline-block;
  padding: 0.5rem 1.5rem 0.6rem 1.5rem;
  background: #efd6da;
  border: 1px solid #cccccc;
  border-radius: 0.25rem;
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  z-index: 99;
  text-decoration: none;
}

.skip-link:not(:focus) {
  clip: rect(0 0 0 0);
  height: auto;
  margin: 0;
  overflow: hidden;
  padding: 0;
  position: absolute;
  width: 1px;
  white-space: nowrap;
}

The skip link is styled to look like a button and uses pretty straightforward styles to do that. The real interesting stuff is what follows after that.

Firstly, we’re using the :not pseudo-selector to add styles to the button when it doesn’t have focus. Inside that rule, we are using the same CSS as this visually hidden quick tip. This visually hides the element, but allows assistive technology, such as screen readers, to still access it.

When you refresh and hit your tab key, it should look like this:

A skip link visible in the top right of the main view

The main course: styling the media browser permalink

We’ve danced around the edges for long enough now: let’s get into the main course and get the media browser layout styled up.

While you’re still in global.css, add the following to it:

Code language
css
.media-browser {
  display: flex;
  flex-direction: row-reverse;
  flex-wrap: wrap;
  position: relative;
  border-radius: 0.5rem;
  overflow: hidden;
  border: 1px solid #cccccc;
  box-shadow: 0 0 2.5rem 1rem rgba(0, 0, 0, 0.1);
}

This is the main layout that sits the elements side-by-side. Like we covered in the skip link section, we have flipped the video and the nav visually by using flex-direction: row-reverse.

Right now, this layout isn’t very helpful, so let’s fine-tune it. Add the following to your global.css file:

Code language
css
.media-browser__nav {
  display: flex;
  flex-direction: column;
  flex-basis: 16rem;
  flex-grow: 1;
  position: relative;
  z-index: 1;
}

.media-browser__video {
  flex-basis: 0;
  flex-grow: 999;
  min-width: 60%;
  background: black;
}

This here is how you get a responsive media player without using a single media query. We’re using the same principles that we covered in the Sidebar in Every Layout.

By setting the nav to have a flex-basis of 16rem and the video to have a flex-basis of 0, we are assisting the browser’s flexbox calculation of available space. Then, because the video has a flex-grow value of 999, we’re hinting to the browser that it should let the video fill all available space as a top priority. We want our sidebar to grow too, so by adding flex-grow: 1 to that, we send another clear message where the spacing-filling priority lies.

We further assist this layout by adding a min-width of 60% to the video element. This is to make sure we don’t end up with a situation where the nav and video are both equal widths, which in this context, would be pretty rubbish.

Our main .media-browser element has flex-wrap: wrap set, so when flexbox can’t fit these two elements—using basis rules we set—in line with each other, they will stack on top of each other. Because they both have flex-grow, they will be 100% wide.

Lastly, even though we reversed the elements visually: this reversal does not apply when they are stacked, which means that on small viewports, the video is at the top and the nav is at the bottom.

Now that is some proper CSS layout!

Making the nav shrinkable permalink

If you look at your project now, it should look like this:

The main layout of the responsive media browser is now complete, just with mostly un-styled links

That nav is at risk of pushing the aspect ratio of the video out if it gets longer than the height of it. To mitigate this in the old days, we’d get our friend absolute positioning out for this sort of problem, but today, we have modern CSS to help us, so we don’t need to bother with archaic hacks.

In your global.css, add the following:

Code language
css
.media-browser__nav > * {
  flex-basis: 0;
  flex-grow: 1;
  overflow-y: auto;
  position: relative;
  min-height: 10rem;
}

This selector targets the <nav>’s direct children, which in this case, is an <ol>. If I just quickly refer back to the .media-browser__nav further up, we already added flex rules like so:

Code language
diff
.media-browser__nav {
  display: flex;
  flex-direction: column;
}

Because we have that CSS set on the nav, the flex rules on our <ol> apply. On that <ol>, we have flex-basis set to 0, which means effectively, we’re telling the browser that we don’t expect it to be anything but 0 high by default. What we do though, is set flex-grow to be 1, which means it should take the available space that the nav leaves for it.

Also on the <ol>, we set the vertical overflow to be auto which then allows the nav items to scroll, which in turn, gives us the desired maintained aspect ratio. We add a min height of 10rem to make sure that when the .media-browser stacks at smaller viewports, the flex-basis: 0 doesn’t make the <ol> vanish.

Adding some style permalink

The layout is done and working great. You’ve got a handy layout primitive to work with now! Let’s add a bit of style though.

In your global.css, add the following:

Code language
css
.media-browser__links > * + * {
  border-top: 1px solid #cccccc;
}

.media-browser__links a {
  display: block;
  padding: 1rem;
  text-decoration: none;
  line-height: 1.2;
  background: #f3f3f3;
}

.media-browser__links :hover {
  background: #e5bec4;
}

.media-browser__links :focus {
  outline-offset: -0.25rem;
}

That’ll turn the links into nice, tap-friendly blocks. Let’s give the active link—which because we’re using links, allows us to treat it as an active page—a bit of treatment. Because it’s an active page, we can use an aria-current="page" attribute, which tells assistive technology like screen readers that the <a>’s href attribute is in fact, the page we are currently on.

This also gives us a handy style hook, using the Exception Principle from CUBE CSS.

Add the following to global.css:

Code language
css
.media-browser__links a[aria-current='page'] {
  border-left: 0.4rem solid #444444;
}

Adding a scrollbar permalink

Because CSS is a hell of a powerful programming language: we don’t need to mess around with JavaScript to get a nice, custom scrollbar for those auto-scrolling nav links.

Add the following to your global.css:

Code language
css
::-webkit-scrollbar {
  height: 0.5rem;
  width: 0.5rem;
}

::-webkit-scrollbar-track {
  background: #f8eef0;
}

::-webkit-scrollbar-thumb {
  background: #444444;
}

/* Color order: thumb, then track */
* {
  scrollbar-color: #444444 #f8eef0;
}

Wrapping up permalink

There we have it, a flexible, responsive and progressive media browser that is achieved with not much CSS at all.

I hope this has shown you how powerful modern CSS layout is—especially when you approach it in a flexible manner, rather than pixel-pushing and forcing the layout to work.

You can download a zip of the final version of this tutorial or go ahead and check out a git repository of it.

Until next time, take it easy 👋

Hello, I’m Andy and I’ll help you to level up your front-end development skills.

I'm a designer and front-end developer who has worked in the design and web industries for over 15 years, and in that time, I have worked with some of the largest organisations in the world, like Google, Harley-Davidson, BSkyB, Unilever, The Natural History Museum, Oracle, Capita, Vice Media and the NHS.

On Piccalilli, I share my knowledge and experience to make you a better front-end developer.

I'm the founder of Set Studio, a creative agency that specialises in building stunning websites that work for everyone. Check out what we're all about.