Reserve space and keep aspect ratio of not loaded yet images for responsive design


#6

Testing with an invalid url will point to no image at all and there will be no intrinsic ratio for width and height and therefore the width attribute will be honoured but as the height is auto in css then it will be zero because there is no image and therefore just big enough to cover the alt content. How can the browser know what the intrinsic ratio of a non existent image is going to be?

On the other hand if the image is available then a browser 'may' work out the intrinsic ratio from the image attributes even if you set height to auto in css. This is at the browsers discretion anyway as image attributes are indeed optional although it is still best practice to add them.

I see no jump in your codepen example but of course that may not be the same for a slow connection.

I'm guessing that if you added a style tag in the html after the img and you set the css to auto you would get the image reserved first before css set it to auto. Of course having a style tag in the body is messy although now allowed in html5. (This won't work for invalid urls because there will be no image to fetch and this the css will be honoured very quickly).

The re-flowing of content is more of an issue these days because of the way flex and grid have to rely on circular references to work out the dimensions of the layout so even if you squash the image problem there will still be some layout re-positioning anyway.


#7

Yes, I understand it now, when I use height:auto then the height attribute is overridden so its value is ignored.

I was somewhat wrong in saying that image width and height attributes are not respected in modern browsers - they are but there is a difference between a broken image (invalid URL) and an image waiting to be downloaded. For broken images (which I initially tested) the attribute dimensions are indeed ignored for inline images and the <img> size falls back to alt content size. But if a browser waits for an image to be downloaded then it reserves space for it based on the attributes. I have now tested it with an image that is loaded with a delay and that's how it works. So all is well with the browsers...

This is correct but still it would be good to minimize the re-flowing by at least getting the images to reserve their space immediately.

This problem seems to fall under the more general issue that CSS lacks a proper way of scaling elements proportionally. If I were able to set max-height proportional to the image's width then this would solve the problem gracefully.


#8

How about using PHP file_exists, file_get_contents and getimagesize.


#9

But that is irrelevant here, image dimensions are already known so no need to employ PHP for this task.

Well, it's a shame there is no native HTML/CSS way to handle this problem for responsive design. On some web sites I experience a lot of page re-flows, especially on mobile, and it's irritating when the content you are reading or are trying to tap constantly jumps off and runs away. I've been using this CSS universally and was happy:

img {
  max-width: 100%;
  height: auto;
}

but now I'd better at least disable this on desktop layouts because then it doesn't do anything apart from turning off space allocation in browsers. For certain use cases I might try applying proportional dimensions using vw units (as described in the article I linked to) but it's not a universal solution and might require some tweaking with unit calculations.


#10

I was thinking about when the "when the image URL is broken".

Have a function displayImg( $imgUrl ) return with a broken image template on failure otherwise return the requested image.


#11

Yes it would be nice to have an aspect ratio property in the css so that you could use it as a multiplier for the height rather than auto and therefore preserve the height. I believe that link you provided above shows a draft spec for something like that. For dynamically added images you would probably need an aspect ratio attribute on the image also if you didn't know the value beforehand so I guess its not that straightforward.

If you have the image widths and dimensions available beforehand you can 'hard code' the image container's height based on the image aspect ratio and thus preserve the space in a responsive layout.

e.g.

It uses the old padding percentage trick (which @coothead also used).

It's a lot more work of course but does solve the problem. I think we will probably have to wait a bit longer until it is solved properly though. Unless some bright spark can improve on this or has a better idea :slight_smile:


#12

I also questioned the relevance of this when I saw it. But I have done similar things myself, creating an image object class with methods to gather various useful properties like aspect ratio, etc. which can be used for things like setting reduced dimensions. And yes the first thing it does is see if the image file actually exists.
Now looking at the solutions offered, using the "padding trick" which is maybe not the most elegant, but Hobson's choice probably.
Perhaps php could help in doing the donkey work and number crunching to render the image in its correctly proportioned container with a padding value relating to aspect. Maybe, dare I suggest it, in-lining the padding value to individual containers. If there are many images of varying sizes and aspects, that may actually be neater than rendering a whole bunch of dynamically built css classes in the head/style and repeating them all in the container's attributes.
Not sure if this suits your needs, but an idea.


#13

This seems to work (somewhat). i.e. giving the img display: inline-block

<!DOCTYPE HTML>
<html lang="en">
<head>
<title>No Image</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style type="text/css">
.nonexistent {
 display: inline-block;
 outline: 1px solid #070;
 color: #f00;
 max-width: 100%;
 min-height: auto;
}
p { outline: 1px solid #007; }
</style>
</head>
<body>
<h1>No Image</h1>
<div>
 <p>leading text content</p>
 <img class="nonexistent" src="nonexistent.jpg" height="200" width="600" alt="nonexistent">
 <p>trailing text content</p>
</div>
</body>
</html>

#14

Trying it out on the original codepen by @Lemon_Juice it seems to work with display: block too. It's the min-height instead of just height that appears to do the trick.


#15

Am I missing something?

I don't see it scaling to preserve the aspect ratio. It stays at 200px height no matter what the width is.

I just did a live edit in the codepen too and only the img width changes.


#16

Yes, my test was not too thorough. :grimacing:
Thought that was just too easy.


#17

Unfortunately, the easy trick is too easy to work...

I see this is a way of automating the implementation of a hack but for now I want to explore ideas without server side languages. Besides, I'm not too fond of the padding trick because it requires using wrapper elements. Actually, I'd like to find a method that will work for user-editable content like a html editor in an admin panel where the site owner can insert html text with normal images that have width and height attributes and things will just work - using wrapper divs adds complexity to this process.

Anyway, I've tried using the vw method and all looked well except that vw refers to the width of the viewport including the scrollbars so if scrollbars appear the proportions are distorted (btw, who the hell thought vw would be any good if they include scrollbars, which are outside of the page content??). So I resorted to javascript and css variables for help:

<!DOCTYPE HTML>
<html>
<head>
<title>Image</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style type="text/css">
body {
  margin: 10px;
}
html {
	--img-horizontal-spacing: 20px; /* fallback value when js is off */
	--img-max-width: 100%;
}
.img {
	outline: 1px solid green;
	max-width: var(--img-max-width);
}
</style>
</head>

<body>
<script>
function setHSpaceVar() {
	var scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
	document.documentElement.style.setProperty('--img-horizontal-spacing', (20 + scrollbarWidth) + 'px');
}

setHSpaceVar();
window.addEventListener('resize', setHSpaceVar);
</script>

<div>
  <p>leading text content</p>
  <img src="delayed-img.php?600x300" width="600" height="300" class="img" style="max-height: calc((100vw / 2) - (var(--img-horizontal-spacing) / 2))"  alt="My Image">
  <p>trailing text content</p>
  <img src="delayed-img.php?700x200" width="700" height="200" class="img" style="max-height: calc((100vw / 3.5) - (var(--img-horizontal-spacing) / 3.5))" alt="My Image 2">
  <p>trailing text content 2</p>
</div>

</body>
</html>

Here is the live example, the images are intentionally loaded with a delay to be able to see if the space is reserved for them before they appear. Not an ideal solution since I still need to use the ugly style attributes where I set the ratios.

Another attempt with no additional html but with javascript that handles setting image proportions and gracefully falls back to the height: auto behaviour when js is off:

<!DOCTYPE HTML>
<html>
<head>
<title>Image</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style type="text/css">
body {
  margin: 10px;
}
.img {
	display: inline-block;
	outline: 1px solid green;
	max-width: 100%;
	height: auto;
}
</style>
</head>

<body>
<script>
function setImgAscpectRatio() {
	var img;
	var imgs = document.querySelectorAll('img.img');
	
	for (var i=0; i<imgs.length; i++) {
		img = imgs[i];
		
		var ratio = img.getAttribute('width') / img.getAttribute('height');
		var maxWidth = img.parentNode.offsetWidth;
		var targetWidth = Math.min(img.getAttribute('width'), maxWidth);
		var height = targetWidth / ratio;
		img.style.height = height + 'px';
		img.style.width = targetWidth + 'px';
	}
}

// set image sizes when page html is partially loaded:
var arInterval = setInterval(setImgAscpectRatio, 1000);

document.addEventListener('DOMContentLoaded', function() {
	clearInterval(arInterval);
	setImgAscpectRatio();
	window.addEventListener('resize', setImgAscpectRatio);
});
</script>

<div>
  <p>leading text content</p>
  <img src="delayed-img.php?600x300" width="600" height="300" class="img" alt="My Image">
  <p>trailing text content</p>
  <img src="delayed-img.php?700x200" width="700" height="200" class="img" alt="My Image 2">
  <p>trailing text content 2</p>
</div>

</body>
</html>

Live demo is here.

This is the least obtrusive method to me because there is no need to change html markup with special attributes or wrapper elements. I wish I could do it with pure CSS with a one-liner but well, we don't live in a perfect world. From usability point of view it appears to be good enough for me.


#18

I like the second version too. After much trial and error I also came to the conclusion that CSS alone could not do it and went to JavaScript. Mine was a bit different, I was trying getComputedStyle and not offsetWidth and would have needed an added wrapper element. So yours is a lot better IMHO. :thumbsup: .


#19

After a bit of testing I found it was not yet perfect in Firefox. The problems I found in Firefox:

  1. When using height: auto I had to also set display: inline-block on the image otherwise javascript was incapable of setting dimensions of images that were not yet downloaded and they shrank to alt text size. I have no idea why display: inline-block is necessary since it doesn't really affect image rendering as opposed to the default display: inline. This was not problematic in this case but sometimes I might want to apply different display rules on images so this method would not allow me to do that.

  2. Although Firefox reserved space for images it didn't reserve space below images that is usual for inline images with the default vertical-align: baseline rule. That space appeared only after the image was downloaded and displayed, which resulted in a slight shift of content under the image. Also, I have no idea why Firefox has to wait for the actual image in order to apply that space but that's what it does in this case. Interestingly, getting rid of height: auto solves the problem, which is also weird since javascript overwrote height immediately so theoretically it shouldn't be a problem...

So finally, I got rid of all CSS styles for images, which gets rid of these problems and is more universal as it allows for more image styling options. The downside is that without javascript the images will not scale at all and will show at their default (large) size. I don't think this is a real problem because from what I can see mobile browsers don't even have the option to turn off javascript - maybe some do but it must be extremely rare for someone to turn off js on a mobile phone. There it goes:

<!DOCTYPE HTML>
<html>
<head>
<title>Image</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style type="text/css">
body {
  margin: 10px;
}
.img {
	outline: 1px solid green;
}
</style>
</head>

<body>
<script>
function setImgAscpectRatio() {
	var img;
	var imgs = document.querySelectorAll('img.img');
	
	for (var i=0; i<imgs.length; i++) {
		img = imgs[i];
		
		var ratio = img.getAttribute('width') / img.getAttribute('height');
		var maxWidth = img.parentNode.offsetWidth;
		var targetWidth = Math.min(img.getAttribute('width'), maxWidth);
		var height = targetWidth / ratio;
		img.style.height = height + 'px';
		img.style.width = targetWidth + 'px';
	}
}

// set image sizes when page html is partially loaded:
var arInterval = setInterval(setImgAscpectRatio, 1000);

document.addEventListener('DOMContentLoaded', function() {
	clearInterval(arInterval);
	setImgAscpectRatio();
	window.addEventListener('resize', setImgAscpectRatio);
});
</script>

<div>
  <p>leading text content</p>
  <img src="delayed-img.php?600x300" width="600" height="300" class="img" alt="My Image">
  <p>trailing text content</p>
  <img src="delayed-img.php?700x200" width="700" height="200" class="img" alt="My Image 2">
  <p>trailing text content 2</p>
</div>

</body>
</html>

Live demo: here.


#20

As far as I can ascertain from the specs an image is by default an inline element and inline elements do not accept dimensions. However once the image has been downloaded it becomes a 'replaced' element and then its paint areas is defined by its intrinsic dimensions or by any attributes specified or by CSS (basically once it has replaced content it behaves much like inline-block).

Therefore for an image not downloaded yet (or not expected to be available) or for an image with a broken url it should default to an inline element and not accept dimensions. Both Chrome and Firefox show this behaviour for missing images while IE/Edge will still honour dimensions which If I am correct is contrary to the specs.

Imagine a user browsing who has specifically turned images off as they are on a slow connection only to find the page full of great big gaps where the images should have been. This makes the reading of the page very awkward. You may help some users but then you break the experience for others. Unfortunately these conundrums are the life of a web developer - you can't always please everyone it seems. :slight_smile:

I'm not keen on this as it makes my browser 'janky' to use when resizing and will be quite an overhead for a page with a lot of images to manage. You can throttle/debounce of course but then you get the image only changing at the end of the resize and sometimes being wider than the window while this is happening which I find just as irritating.

I think you need to be careful that in solving the initial layout problem you don't degrade the ongoing and continual performance of the users browser. I'm not saying what you are doing is wrong and I see it as a good exercise to make things better but I often see that in attempts to solve one problem we actually make it worse further down the line in some un-associated way.

For me the initial page load is part and parcel of the web experience and I'm quite happy for things to move and settle down (usually imperceptibly on a fast connection). I am not happy though to have my browsers stutter every time I try to resize it to make room for other apps on the desktop. My case may be unique but I am a constant 'resizer' as I have many applications open and I like to organise them myself.

I am old school and in the olden days an inline element was not valid as a direct child of the body and for that reason I tend to wrap all my images in a div (or p element). I realise times have changed but old habits die hard and I have found that having your image in a wrapper is very useful in the long term as it offers a nice hook to change and adjust things further down the line or create layout tweaks and changes. However this is a personal preference only and not having a wrapper for an image is perfectly valid although I would still argue the semantics of it. However semantics seem to have slipped a little in html5 - although that has made things easier I guess for most:).

All in all though this a very interesting thread and in the end it probably boils down to a personal preference and I guess you do what you think best :slight_smile:


#21

I'm an old timer too, I remember the problems that happened when inline elements were children of the body and siblings with blocks. Besides that, the first thing that comes to mind for me is image captions. A wrapping div around the image paves the way for captions. Think about that client that comes along after you've write the html for 50+ pages and says, " I think I want some Captions under my images".

To bring the html up to speed with html 5 one could consider using the <figure> element. It was also old timers that helped write the html5 specs. There's a reason the <figure> is a block element that accepts Flow content.

Take a look at the html structure for the figure, at it's bare minimum you would get this so you wind up with block children of the body...

<!-- Just a figure -->
<figure>
  <img
  src="https://developer.cdn.mozilla.net/media/img/mdn-logo-sm.png"
  alt="An awesome picture">
</figure>
<p></p>

#22

Yes good example :slight_smile:

That's also reminded me that replaced elements cannot have the pseudo elements :before or :after applied (unless its a broken image and then not replaced) which means that you have no hooks for anything fancy you may wish to do later on. Having a wrapping element also immediately gives you access to these pseudo elements should you wish 'non-content' embellishments.:slight_smile:


#23

Great, I didn't know that some of my doubts about how images are displayed are actually documented in the specs! But in this particular case I think you are partially right and partially wrong. It is correct that an image by default is an inline element and does not accept dimensions but it becomes a replaced element when the element already has intrinsic dimensions (e.g. from the dimension attributes or CSS rules) and the user agent has reason to believe that the image will become available and be rendered in due course - second case in this paragraph. So images waiting to be downloaded, to my mind, should accept dimensions (but broken images shouldn't as you observed). I think this was an unusual combination of initial height: auto followed by setting dimensions in js that threw Firefox off.

I haven't thought of this use case but you are certainly right and therefore broken images or those not meant to be downloaded certainly should stay inline and not accept dimensions. From my tests all browsers except IE and Edge follow this behaviour and these experiments of mine with js do not affect it.

This is a good point, we shouldn't cause slow downs for browser resizing (I'm also a 'resizer'!). This would need some testing to know how much this script affects the speed. However, just for fun I put the for loop in my script in another for loop with 1000 iterations and I noticed only a slight slow down in browser resizing, for 10 or 100 iterations I didn't even notice - this would indicate there is quite a lot of headroom. And still, the slow downs don't affect main browser window resizing since modern browsers handle content in a separate process so only a few fps drop might be noticeable within the browser window.

Anyway, wanting to improve it I've come up with an idea that we could cut down on performance penalty if we use js to resize only images which are waiting to be downloaded - those that are fully available can be safely delegated back to pure CSS resizing with height: auto. This would go like this:

<!DOCTYPE HTML>
<html>
<head>
<title>Image</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style type="text/css">
body {
  margin: 10px;
}
.img {
	outline: 1px solid red;
}
</style>
</head>

<body>
<script>
function setImgAscpectRatio() {
	var imgs = document.querySelectorAll('img.img:not([data-loaded])');

	for (var i=0; i<imgs.length; i++) {
		var img = imgs[i];
		
		if (!img.complete) {
			// image not loaded: resize with js
			var ratio = img.getAttribute('width') / img.getAttribute('height');
			var maxWidth = img.parentNode.offsetWidth;
			var targetWidth = Math.min(img.getAttribute('width'), maxWidth);
			var height = targetWidth / ratio;
			img.style.height = height + 'px';
			img.style.width = targetWidth + 'px';			
		} else {
			// when image is loaded apply pure CSS resizing from now on
			img.style.outlineColor = 'black';
			img.setAttribute("data-loaded", "1");
			img.style.removeProperty("width");
			img.style.maxWidth = "100%";
			img.style.height = "auto";
		}
	}
}

// set image sizes when page html is partially loaded:
var arInterval = setInterval(setImgAscpectRatio, 1000);

document.addEventListener('DOMContentLoaded', function() {
	clearInterval(arInterval);
	setImgAscpectRatio();
	setTimeout(setImgAscpectRatio, 0);
	window.addEventListener('resize', setImgAscpectRatio);
	window.addEventListener('load', setImgAscpectRatio);
});
</script>

<div>
  <p>leading text content</p>
  <img src="600x300.php" width="600" height="300" class="img" alt="My Image">
  <p>trailing text content</p>
  <img src="delayed-img.php?700x200" width="700" height="200" class="img" alt="My Image 2">
  <p>trailing text content 2</p>
</div>

</body>
</html>

Live demo.

I set outline colour to black only to indicate the image has been put back to CSS resizing. This should have very minimal performance penalty once the images have been downloaded. There have a been a few issues I needed to deal with, for example IE doesn't fire onresize events before all images get downloaded so js resizing does not work then - so I added the window.onload event just to ensure things are displayed properly in the final stage. I also had to add setTimeout(setImgAscpectRatio, 0); in the document ready event to handle certain cases when scrollbars could have been erroneously included in calculation of 100% width of the parent element.

This could be further enhanced with throttle/debounce techniques if necessary (but I think that would be extreme). I feel the more I dig into it the more subtle behaviours and issues come up to cope with and this also makes me think whether it's all worth the hassle. I have a feeling I'm trying to code something that browsers should be able to do on their own with a one-line CSS rule...

Well, yes, I certainly agree that it's good practice not to use dangling inline elements but this is a somewhat different topic altogether. Here I was rather concerned with using wrapper elements for the purpose of applying proportional dimensions with the padding trick. To keep html clean I want a universal solution, for example, I will usually have page content like this:

<p>This is some text.</p>
<p><img src="img.jpg" with="500" height="255" alt="my dog"></p>
<p>This is some more text.</p>
<div style="float: right"><img src="img.jpg" with="200" height="100" alt="my snake"></div>

This might come from an editor in an admin panel or some outside source and I just don't want to be concerned with fixing the html just for the purpose of image scaling on small screen devices. I simply want this html to just work on any device without modifications. That's why I said I don't want to use wrapper divs for this purpose.


#24

Yes that's why I pointed to that part in the specs and my take on it is that if the image url is invalid/missing which I guess will be ascertained very quickly (as no image will need to be downloaded) the browser will almost immediately determine that the image is not 'available' and therefore not behave like a replaced element.

Reading specs is always difficult and you often have to read between the lines or accept some ambiguity so I may be wrong. However that is certainly how all browsers except IE/Edge seem to treat them.

I usually set my images to display:block in my reset styles by default anyway as that is how I like them to behave most of the time.:slight_smile:


#25

I found it very interesting to experiment with. I guess to get a better understanding it would require digging into actual browser engine code. My conclusions, as wrong or right as they may be are:

Giving an image height: auto results in it getting height: 0px unless it loads in. I had assumed it would fallback to the attribute value. Thinking about it more I guess either could make sense depending on which was more important, "wasting" screen real estate to preserve layout design or "saving" screen real estate at the possible expense of disrupting screen layout.

When an image fails to load, the alt text is displayed as unwrapped text. In some ways it can be styled as though it was <img>alt text</img> (eg. it accepts color style), but frustratingly, for the most part the alt text behaves as though it is not part of the DOM.

This adds to my "CSS wishlist":

  • a "parent" combinator (though :not looks promising)
  • element level @media queries