Responsive is relative

So uptil now we have seen how to avoid document reflow on lazy loading an image by predefining its width and height values in the previous Image Dimensions chapter. The idea works pretty well on larger devices however this is not the case with smaller ones.

Absolute dimensions for an image aren't responsive at all - on a small screen, images with absolute width and height values might go out of the viewport. What we need instead are relative dimensions and hence responsive images.

Making the vertical dimension relative

Responsive designing which is designing sites that scale perfectly on different devices is of paramount significance these days when a large percentage of internet users comes from mobile devices.

Therefore we have to resort back our image's dimensions to percentage values, but there is a slight problem with this too. For width we can use 100% to fit the container element into the available width, but for height we can't use percentage values.

See the explanation for why does height not take percentage values in some occasions.

Hence we have to use some other thing apart from height which can be responsive i.e work with percentage values and increase the area of the .lazy element to match the natural proportions of the image. Can you think of any?

Which of the following properties can give .lazy some visual vertical height, apart from height?
margin
padding
min-width
max-width

The solution is to use padding and since we are only concerned with the vertical dimensions i.e the height we can use padding-bottom or padding-top instead too; whatever you like. We will go with padding-bottom.

padding-bottom can take percentage values which are relative to the 100% width of an element. It is the property we will be replacing height with, but we aren't over even as of yet!

The last question is what value to give to padding-bottom?

If an image has a width of 500px and height of 125px what would be the height of the image relative to its width which is considered to be 100%. In other words, what percent of the width is the height?

To calculate this we simply divide the height by the width and multiply by 100. The answer is 25% in this case.

What we need is something along these lines:

padding-bottom:(height / width) * 100

Therefore now we can alter the backend function getDim() to throw in a string containing padding-bottom as well.

<?php
    function getDim($src) {
        $size = getimagesize($src);
        echo 'style="padding-bottom: ' . ( ($size[1] / $size[0]) * 100 ) . '"';
    }
?>

Restructuring the HTML

Now that we know how to calculate the value of padding-bottom it's time to incorporate it in the HTML markup. So where to place padding-bottom?

padding-bottom has to go inside .lazy not .lazy-img because if we give it to .lazy-img then the actual image will have a padding which we would've to remove when it is loaded. With .lazy we can simply give it a padding and then position the lazy image inside it using absolute positioning.

So in the first step of restructuring our markup we include the PHP code within .lazy, NOT .lazy-img, as we did in the previous chapters.

<div class="lazy" <?php getDim("image.png"); ?>>
    <img class="lazy-img" data-src="image.png">
</div>

So the first thing done - now over to the second problem.

Although padding-bottom does indeed solve the responsive issue, it introduces yet another problem into the HTML.

If we give padding-bottom to .lazy then its value, whatever it be - 30%, 40%, 90%, will be relative to the container of .lazy which in our case is the body element. This would mean that even on devices where our image has a lesser width than body, we will have to fit it in it.

The solution is very simple - give another container element to .lazy so that .lazy resizes relative to that element, not body. We'll call this element .lazy-wrapper.

<div class="lazy-wrapper">
    <div class="lazy" <?php getDim("image.png"); ?>>
        <img class="lazy-img" data-src="image.png">
    </div>
</div>

But we aren't over even yet! Yes sometimes things in HTML and CSS do become a lot more involved!

We finally need to give .lazy-wrapper a width dimension so that the lazy image appears on larger devices at its natural dimensions (and not filling any container's width).

Which of the following property we would've to give to .lazy-wrapper to make is responsive on any device size.
width
min-width
max-width

Out of the three choices given above, we would've to go with max-width and give it the natural width of the lazy image. There is a very simple reason to it - just think what max-width does?

max-width restricts an image to go beyond a given value. When we give, let's say max-width: 640px to the image we've been seeing in our examples, the property makes sure that we don't exceed 640px in terms of the width.

If we had chosen width instead, it would've worked but with more code! width: 640px would make the .lazy-wrapper be 640px wide, and without any further code, make the image go out of the viewport on smaller devices. Same goes for min-width. Although this issue can be tackled using @media queries, max-width turns out to be the simplest and the quickest choice.

So in conjunction to all this discussion, let's give .lazy-wrapper a max-width property holding the natural width of the image.

To do this we can as before get the job done manually or either write another PHP function, or yet redefine getDim() rigorously.

Following we define another function getWidth() to take care of this job:

<?php
    function getWidth($src) {
        $size = getimagesize($src);
        echo 'style="max-width: ' . $size[0] . 'px"';
    }
?>
<div class="lazy-wrapper" <?php getWidth("image.png"); ?>>
    <div class="lazy" <?php getDim("image.png"); ?>>
        <img class="lazy-img" data-src="image.png">
    </div>
</div>

Since in the code above we are merely repeating the call to getimagesize(), we can instead redefine getDim() rigorously to write all the markup for an image, given its source. We will now call the function lazyImage() instead.

<?php
    function lazyImage($src) {
        $size = getimagesize($src);
        echo '<div class="lazy-wrapper" style="max-width: ' . $size[0] . 'px"><div class="lazy" style="padding-bottom: ' . ( ($size[1] / $size[0]) * 100 ) . '%"><img class="lazy-img" data-src="' . $src . '"></div></div>';
    }
?>
This PHP code won't work for root-relative paths such as /Images/Local etc. To make it work for them you will have to first check if $src is a root relative path and if it is then prepend it with $SERVER["DOCUMENT_ROOT"].

Now we will only have to write the following to produce a lazy image.

<?php lazyImage("image.png"); ?>

Laying out the CSS

And now we have to apply some CSS to get the actual lazy image, which is .lazy-img, not affected by the padding (padding creates an inner spacing within an element so that its children are placed beyond that space).

Consider the following CSS and read along the comments to understand each property's purpose.

.lazy {
    position: relative;
    width: 100%;
}
.lazy-img {
    position: absolute;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
}

In line 2, position: relative makes the .lazy container get its image scale relative to it. width: 100% makes the image container i.e .lazy fit into .lazy-wrapper. Without it .lazy, would have 0px of width and no image will be seen in view.

position: absolute gets the .lazy-img position and scale freely inside its container. Lines 6-7 make it fill the dimensions of its container and lines 8-9 make it position in the correct place. Without lines 8-9 the image will be positioned below the padding of .lazy.

And with this code set up we have finally arrived at a fully responsive, reflow preventing lazy loader. Now we can set the container of .lazy to any dimension and it would scale relatively and elegantly.

Responsive Lazy Loader