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.
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:
- Download these starter files
- Extract the contents
- Open your terminal inside the folder and run
npm install
- Serve up and watch for file changes with
npm start
You can skip this process and build a more static version of the site using a CodePen template if you’d prefer.
All you need to do is click here and a brand new pen will start up with all the stuff you need, ready to go.
When you run npm start
and open your browser at http://localhost:8080, you should see something that looks a bit like this:
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.
You might be thinking, “but in the completed demo, the links are at the start, on the left”, which is true, but because we will be visually changing the source order, we need to make sure we are not creating a problem for keyboard users.
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.
When you add an aria-label
to a <nav>
element, you don’t need to add the word, “nav” or ”navigation” because the screen reader already knows this and will already announce it.
It’s the same type of rule as not adding the word “image” to an <img>
alt
attribute, because screen readers will announce them as images.
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.
This approach of managing stacking with flow and rhythm is something I’ve written extensively about for a while, but check out this and most importantly, this for a high-level overview.
Your version of the project should look a bit like this now:
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:
- Divide 9 by 16:
0.5625
- 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?
Styling the skip link permalink
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:
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
.
Be very careful changing the source order of content—especially when there are focusable elements at play (<a>
, <button>
). Changing the visual order of elements with CSS doesn’t change their source order, so tabbing can be very unpredictable for keyboard users
I’d say we are right on the edge of being a nuisance for keyboard users with this example and the skip link mitigates some of the problems that we have introduced.
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:
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 - don’t copy
.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.
Because the .media-browser__nav
is a child of .media-browser
, which has display: flex
set, it takes advantage of flex’s default alignment of stretch
, which does what it says on the tin: makes its children stretch, vertically.
This means that the space for the <ol>
is automatically made available, as long as the media browser isn’t stacked.
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 👋