Fit-To-Screen With CSS

In a recent side project with the infamous Viktor Vörös, we quickly designed and shipped an MVP in a few days. With such a small timeframe, we embraced a mobile-first approach; We didn't want to dwell on a desktop design until the application is usable on mobile. With his detailed work in place, a pixel-perfect responsive implementation was needed to reproduce the same feeling on all mobile devices.

The requirement was clear:

  • make a pixel-perfect implementation based on the 320px wide PSD design
  • if the screen is wider, scale the application to fit the screen width, with the fragile balance of sizes and spaces intact
  • if the screen is wider than 640px, do not scale anymore. Instead, it should stay 640px wide.

With these in mind, there are three things to consider.

Scale Spaces

The first part is to define scalable paddings and margins. Percentages are extremely helpful to define paddings and margins, because of one very important detail:

percentage is relative to the width of the containing block.

So, to scale spaces while keeping the existing ratios, you have to set your vertical space relative to the horizontal space. In the example below, I wanted to set vertical space exactly the double to the horizontal space.

Scale Sizes

Unfortunately, we cannot do the same trick with sizes. The height percentage is relative to the height of the containing block. It makes sense, but we cannot use percentages in widths and heights to keep ratios. The quickest solution is to use the vw unit. Define width and height both 1vw, and you get a square that scales with the screen. However, this is not a flexible solution. It is suitable for one case, but it won't work if we want to use the same component under different sizes.

Luckily, padding worked once, and we can use it here too. Let's say, I want to make the previously seen yellow div a squared div which is half as wide as the green container.

First, we need to set the width of the inner-box to 50%. We know we can use padding to set space relative to the parents' width. So, let's create a div with a class square. Now, if we set padding-bottom to 100%, the size will match the width, we have the desired size. However, there is a huge problem.

The content of the div is zero. If you type anything into the square, it won't be a square anymore: the padding-bottom will equal the width of the parent div, but the additional text adds to the height. To solve this, insert another element that will surely not have any content. Using :after is just perfect. Now, the content defined in after will span the height to square, but the problem still exists.

If you type "Hello" into the square, it will be higher than taller, again. To solve, this, we need to add an extra div inside the square. Let's call it content. If we set its' position to absolute (and don't forget to set square to position: relative), its content won't ruin the height anymore. All we need to do now is to set the content's width and height to 100%, and we have a fully usable square container.

Note, that with padding-bottom, you can set any ratio. For example, if you set it to 150%, you'll get a div with a ratio of 15:10.

Scale Fonts

Widely used responsive font-size units are rem and em. The first sets a size relative to the font-size of the root element, the latter sets it relative to the current element's font-size. Here, we will utilize rem.

We need to set a root font-size that makes sense. Usually, it is 16px by default. I prefer setting it to 10px, as it is very easy to calculate with. So, let's say, given a 16px font-size in a 320px wide design, we need to set font-size to 1.6rem. It shouldn't change anything as of now.

The trick is to dynamically set the font-size of the root; If we correctly set that, it will automatically scale everything else. I normally use a neat mixin, as it is published on CSS Tricks.

@function strip-unit($value) {
  @return $value / ($value * 0 + 1);
}

@mixin fluid-type($min-vw, $max-vw, $min-font-size, $max-font-size) {
  $u1: unit($min-vw);
  $u2: unit($max-vw);
  $u3: unit($min-font-size);
  $u4: unit($max-font-size);

  @if $u1 == $u2 and $u1 == $u3 and $u1 == $u4 {
    & {
      font-size: $min-font-size;
      @media screen and (min-width: $min-vw) {
        font-size: calc(#{$min-font-size} + #{strip-unit($max-font-size - $min-font-size)} * ((100vw - #{$min-vw}) / #{strip-unit($max-vw - $min-vw)}));
      }
      @media screen and (min-width: $max-vw) {
        font-size: $max-font-size;
      }
    }
  }
}

It linearly scales from a minimum width until it reaches a maximum width, after which it won't scale anymore. For the sake of simplicity, I use vw to set the root size.

Set a Maximum Size

Scaling is all good, but it might not be too practical to fit to screen after a certain size. We want to say that, if the screen is as big as a desktop, the image should not grow anymore. For fonts, the easiest way is to use the previously mentioned fluid-type mixin. For paddings, margins, widths, and heights we can use media breakpoints.

TL;DR

  • Use percentages wherever you can.
  • If you need to keep the ratio of width and height, use the padding trick.
  • If you use a framework, you can hide the implementation details of the ratio trick using a component or directive.
  • Set all font sizes relative to the root size (or relative to the parent size using %).
  • To dynamically change the root font size, use the fluid-type mixin.