If you ever needed to embed a responsive video on your site, you probably know how unelegant it is to maintain its aspect ratio on different screen sizes.

If, for example, you wanted to maintain a ratio of 16:9, probably the most popular way right now is to do something like that:

.item { 
    position: relative; 
    padding-bottom: 56.25%;
    height: 0;
    overflow: hidden;
    max-width: 100%;
    height: auto;
} 

.item iframe,
.item object,
.item embed { 
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

The above code sets a fixed height to the object’s container using padding-bootom, whose value is calculated based on the desired ratio using the Rule of Three. The object itself is given a position: absolute, to avoid affecting the container’s height.

If you want to have the same effect on an image, things can become more complicated, as, unlike the video, where you can set width and height to 100%, an image filling the entire available container without a care for its aspect ratio would appear deformed.

A popular hack to work around this issue is absolutely positioning the image, move it 50% to the top and left, and then use transform:translate() to center it horizontally and vertically. To avoid issues where the image is too small or too big for its container, you will probably want to set a min-height and max-height and make sure that the height is set to auto.

.item img {	
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%); 
    min-width: 100%;
    max-width: none;
    min-height: 100%;
    max-height: none;
    width: auto;
    height: auto;
}

It will eventually do the job, but it’s messy, difficult to extend, because of the absolute positioning and, in the end, it’s a hack.

object-fit to the rescue

Luckily, at some point, we were introduced to the object-fit property, which allows us to set how the content of a replaced element (an element whose representation is outside the scope of CSS, like an image or a video) should be resized to fit its container. Using it, we could refactor the previous snippet like that:

.item img {	
    width: 100%;
    height: 100%;
    object-fit: cover;
}

It’s cleaner, more elegant, easier to extend, and probably more efficient too. The only drawback is that it will not work on Internet Explorer.

Still, using padding-bootom and a position: absolute to properly place the element in the container seems counter-intuitive. Not only do we have to calculate every new aspect ratio that we need to support, but we are also restricted on how to structure our markup by the fact that we have to use position: absolute. If only we could dynamically calculate the height of the container based on its relative width…

Actually, we can.

Using calc() and relative length units

If we know the container’s width relative to the screen’s width, we can calculate the required height, using pretty much the same math as before. Only with calc(), it becomes much easier. For example, if we have an element that takes up the 1/3 of the screen and we want it to have a 16:9 aspect ratio, we should simply do the math using something like this:

height: calc((100vw/3)/(16/9));

It should be easy to read: first we divide the screen’s width (100vw) with the number of the columns of our layout, and then we divide that with the result of the division between our ratio’s values. There might be a problem, though.

The above will work only if the elements’ grid takes up the entire screen width and only if there is no gap between them. If they exist within a container with a fixed width and if they are separated by some gap, the height might become completely inaccurate (in fact, on such an occasion we should even take into account the browser’s scrollbar, if we want to be as much precise as possible).

Therefore, we need to take this into account and add it to our calculations. So, supposing that the grid’s container has a maximum width of 1024 pixels and the entries have a gap of 1rem, we should adjust our CSS like that:

height: calc(((100vw/3)/(16/9)) - 1rem);
max-height: calc(((1024px/3)/(16/9)) - 1rem);

All we do here is subtracting the gap from our previous calculation and adding a max-height property which is pretty much the same with the only difference being that it uses the container’s max-width instead of the screen’s width. Eventually, we end up with a code that might look like this:

.container {
  display: flex;
  flex-flow: wrap;
  justify-content: space-between;
  width: 100%;
  max-width: 1024px;
}
.item {
  height: calc(((100vw/3)/(16/9)) - 1rem);
  max-height: calc(((1024px/3)/(16/9)) - 1rem);
}
object,
iframe,
embed,
img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

That’s clean and readable enough for us to easily change its values depending on the layout. Still, there are a few repeated values, and having to adjust it for different screen sizes using media queries might be cumbersome. Let’s try to improve it even further.

Adding @mixins to the mix

To avoid having to memorize the height and max-height calculations, we can use SCSS and store them in a @mixin which would look like this:

@mixin ratio($rw, $rh, $mw, $cols, $gap) {
  height: calc( ((100vw/#{$cols})/(#{$rw}/#{$rh})) - #{$gap});
  max-height: calc( ((#{$mw}/#{$cols})/(#{$rw}/#{$rh})) - #{$gap});
}

The first two parameters are the width and height of the ratio, the third is the maximum width, the fourth is the number of columns and the fifth is the gap. That way, we can simply call it whenever we like, using:

.item {  
   @include ratio( 16, 9, 1024px, 3, 1rem );
}

CSS Custom Properties

If you are not familiar with CSS Custom Properties, you might ask: “If we are gonna use SCSS anyway, why not use their own variables, which have better browser compatibility anyway?”. The answer to that is that native var() can be used inside any selector and can be used inside media queries. On top of that, they can be accessed and manipulated from JavaScript.

Given that the browser support is good enough (they work on all major browsers except Internet Explorer), it would be a shame if we didn’t benefit from the aforementioned features.

So, instead of repeating values and our mixin in the media queries, we can declare our variables at the top of our page container and use them with our @mixin.

.container {
   --ratio-w: 16;
  --ratio-h: 9;
  --max-width: 1024px;
  --cols: 1;
  --gap: 1rem;
  max-width: var(--max-width);
  display: flex;
  flex-flow: wrap;
  justify-content: space-between;
}
.item {
  width: 100%;
  margin-bottom: calc(var(--gap)*2);
  @include ratio( var(--ratio-w), var(--ratio-h), var(--max-width), var(--cols), var(--gap));
}
 img,
iframe,
embed,
object {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

If you are wondering why we used 1 column instead of 3, it’s because we go mobile-first. The beauty of it is that when we need to change the width of the item, we don’t have to redeclare the mixin or change the item’s width. All we need to do is re-set the variable like so:

@media all and (min-width: 960px) {
  .container {
    --cols: 3;
  }
}

To get the full picture, and get a full working code as well, check out the following Pen, which ties up what we’ve seen so far and goes a bit further, using various types of content (text, video, images, maps) and manipulates the CSS properties with JavaScript, as a bonus (Keep in mind that the vw unit doesn’t take into account the browser’s scrollbar, so the accuracy of the embed is decreased).

See the Pen
Maintain ratio on elements with unknown heights and contents
by Giorgos (@gsarig)
on CodePen.

 

Leave a Reply

Your email address will not be published.