Applying Flexbox to the Video Player

23rd October, 2015 — Laura Kalbag

When I added the Internet as a Commons video to the Ind.ie site, our video player had a problem:

The Problem: Inflexibility

“@indie the time display doesn't display the hours on the video embed. #bugreport” — Jelle Verkleij

The containing space for the time on the video player was only wide enough to display mm:ss, and not hh:mm:ss. This meant a video that’s playing at 1:29:02, will only appear to be playing at 29:02.

Screenshot of video showing 29:02 when it’s actually 1:29:02

The layout of the video player uses floats to keep all the buttons in place whilst it resizes to fit the viewport (so it’s responsive).

I designed the CSS so it also shows the buttons exactly in the same order as the HTML source, so when you’re using keyboard navigation, you can move between buttons and the highlighted buttons are exactly as you expect them to be.

However, in order to make this work responsively, with all the elements shifting to fit the viewport space, I’ve used ugly CSS for the progress bar:


.progress-bar {
  width: 85%;
}
@media only screen and (min-width: 480px) {
  .progress-bar {
    width: 90%;
  }
}
@media only screen and (min-width: 480px) {
  .progress-bar {
    width: 92.5%;
  }
}
@media only screen and (min-width: 970px) {
  .progress-bar {
    width: 95%;
  }
}
@media only screen and (min-width: 1795px) {
  .progress-bar {
    width: 97.5%;
  }
}

The width of the progress bar is arbitrary and eye-balled so that the time and progress bar roughly fill the space below the video in different viewport sizes. It’s a lack of flexibility masquerading as flexibility.

For a long time, I wanted to make the video player properly flexible. This bug finally gave me the kick I needed. Enter: flexbox.

The Solution: Flexbox

Flexbox is a CSS layout module aimed at laying out flexible components. If you look at the cross-browser support for flexbox, it initially looks good. But then you look at the known issues, and see a whole lot of caveats.

Progressively enhancing with Modernizr

Still… caveats never stopped me working with new standards, as I’m armed with Modernizr. Modernizr is a wonderful little script that detects support of browser features.

By default, the video player’s CSS works well with more old-school browsers, so it’ll be left as-is for those browsers (with a few tweaks to make the time space wider.)

Modernizr can generate a production-ready (small and quick) script that only detects flexbox and flexwrap support. (The Ind.ie site already uses Modernizr flexbox detection, along with detection for SVG, CSS Shapes, and CSS columns.)

Downloading the custom build of Modernizr including flexbox and flexwrap

Modernizr detects feature support, then adds the supported features as body class names. This makes it easy to progressively enhance CSS as I can add fancy new CSS based on these body classes.

How the body element looks with Modernizr:
<body class="flexbox flexwrap">

This means I can specify CSS for all browsers, and CSS just for those that support flexbox and flexwrap:
.px-video {
    /* do all these things in supported in all browsers */
}

.flexbox.flexwrap .px-video {
    /* do things that are only visible to browsers that support flexbox and flexwrap */
}

Resetting the layout

Once it’s established that the browser has good-enough flexbox support, I can use flexbox without worrying.

video controls on narrow viewport
I write CSS starting with the smallest, simplest layouts, and work my way up, so I invariably start layouts mobile-first

Using the .flexbox.flexwrap class, I can reset the existing layouts so the existing percentage widths CSS don’t play havoc with the flexbox CSS. This involves removing any widths that aren’t button-specific, resetting margins, and any absolute positioning.

Floats and clears don’t have any effect on flex items, so there’s no need to reset those.

This isn’t very exciting (or pretty) CSS, but you get the idea:
.flexbox.flexwrap .progress-bar {
    width: auto;
}

@media only screen and (min-width: 480px) {
    .flexbox.flexwrap .progress-bar {
        width: auto;
    }
}

@media only screen and (min-width: 970px) {
    .flexbox.flexwrap .progress-bar {
        width: auto;
    }
}

@media only screen and (min-width: 1795px) {
    .flexbox.flexwrap .progress-bar {
        width: auto;
    }
}

.flexbox.flexwrap .px-video-progress {
    width: auto;
}

.flexbox.flexwrap .px-video-time {
    margin-top: 0;
}

.flexbox.flexwrap .px-video-playback-buttons {
    min-width: auto;
}

@media only screen and (min-width: 620px) {
    .flexbox.flexwrap .px-video-playback-buttons {
        left: auto;
        margin-left: auto;
        min-width: auto;
        position: relative;
        width: auto;
    }
}

.flexbox.flexwrap .px-video-controls button {
    margin: 0;
}

.flexbox.flexwrap .px-video-volume-controls {
    min-width: auto;
}

@media only screen and (min-width: 540px) {
    .flexbox.flexwrap .px-video-volume-controls {
        margin-top: 0;
    }
}
video controls on narrow viewport shown in source order with no overarching layout
Resetting the layout results in a pile of buttons and inputs showing mostly in their source order. (With the exception of the time which still floating to the right as there’s no flex to override the float.)

Getting started with Flexbox

Flex container

Flexbox can control the containing element (the flex container) and the individual items inside that container (the flex items).

To give the container its flex properties: 
.flexbox.flexwrap .px-video-controls {
    /* enables flex for all its children */
    display: flex;

    /* arrange buttons/inputs in rows, not columns */
    flex-direction: row;

    /* run rows from left-to-right, wrap overflowing items to next row */
    flex-wrap: wrap;

    /* stick buttons/inputs to left/right edges with excess space distributed between */
    justify-content: space-between;

    /* horizontally centre rows inside container */
    align-items: center;

    /* horizontally centre all content inside container when there are multiple rows */
    align-content: center;
}
video controls with only flex container css applied
There’s already some basic flexible layout appearing
video controls with only flex container css applied showing evenly distributed items
And it becomes even clearer as the viewport is widened

Flex items

Looking at the video controls, the ideal is for the time to take up all the space it needs (and no more), and the progress bar to fill the remaining space. The play control buttons should sit on the left side, and the volume on the right, both roughly filling half the space.

labelled controls showing desired layout
Setting the flex properties of the time
.px-video-time {
    flex-grow: 0; /* don't grow */
    flex-shrink: 0; /* don't shrink */
    padding-left: 10px; /* some padding for breathing space */
}
video controls in both narrow and more middling viewports
The time is in the right place on narrower viewports, but when the viewport gets wider, the other items jump up to the first row, as they all fit alongside each other.

In order to get the progress bar to fill the remaining space on the top row, the bar needs to be wide enough that there won’t be room for the other buttons to jump to that row.

Then the magic part is specifying flex-grow: 1…
.flexbox.flexwrap .progress-bar {
    flex-grow: 1; /* flex to fill the rest of the available space */
    width: 75%; /* leaves enough space for the time */
}
video controls in three sizes with the playback buttons in the wrong place on the widest view
The flex is perfect on narrower and middling viewports, but when the viewport gets to ~590px, the playback buttons jump up because the remaining 25% of a wider viewport is a bigger space. Setting a media query here will fix the problem:

.flexbox.flexwrap .progress-bar {
    flex-grow: 1; /* flex to fill the rest of the available space */
    width: 75%; /* leaves enough space for the time */
}

@media only screen and (min-width: 590px) {
    .flexbox.flexwrap .progress-bar {
        width: 85%;
    }
}
video controls on top row showing progress bar and time flexing to take up correct space
Now the top row looks perfect.
video controls with time showing 14500:20:34
Now there could be a video that’s thousands of hours long, and the layout would still flex to accommodate the numbers.

Next the volume controls need to be stuck to the right side, as they’re not flexing to the edges on all viewport sizes. This requires the playback buttons to flex-grow to fill the rest of the space:


.flexbox.flexwrap .px-video-playback-buttons {
    flex-grow: 1; /* flex to fill rest of available space */
    min-width: auto;

}
video controls with playback buttons filling space to push volume controls to far right

Flexboxception

The only layout issue that remains is centring the playback buttons on wider viewports. Ideally play/pause is exactly centre so it’s easier to hit. Here’s where flexbox can be used inside flexbox.

video controls with bigger sized playback buttons stuck on left

First the playback buttons container needs to be made into a flex container:


@media only screen and (min-width: 620px) {
    .flexbox.flexwrap .px-video-playback-buttons {
            /* enables flex for all its children */
            display: flex;

            /* arrange buttons/inputs in rows, not columns */
            flex-direction: row;

            /* run rows from left-to-right, wrap overflowing items to next row */
            flex-wrap: wrap;

            /* stick buttons to centre with excess space distributed on either side */
            justify-content: center;

            /* horizontally centre rows inside container */
            align-items: center;

            /* horizontally centre all content inside container when there are multiple rows */
            align-content: center;
  }
}
video controls with bigger sized playback buttons centred to their container but not to the width of the player
Boom! As easy as that… Although the playback buttons are now centred, but to the container and not the player itself.

To offset the width of the volume and fullscreen buttons, the same width as the left padding needs to be added to the playback buttons:


@media only screen and (min-width: 620px) {
    .flexbox.flexwrap .px-video-playback-buttons {
        display: flex;
        flex-direction: row;
        flex-wrap: no-wrap;
        justify-content: center;
        align-items: center;
        align-content: center;
        padding-left: 180px;
  }
}
video controls with bigger sized playback buttons centred to the width of the player
And it’s pretty much done.

Just a quick margin tweak to align the progress bar to the vertical centre of the time… and it’s looking flexy in loads of viewports with minimal media queries required.

video controls looking flexible in five different viewport sizes

Conclusion

The flex-grow and flex-shrink proportions are the hardest part of flexbox, but with just a few rules you can easily make a component way more flexible than using percentage sizes and media queries. Thanks to the great CSS Tricks Flexbox guide for illuminating a lot of the harder concepts.