Selecting a responsive image size for a background-image with background-size: cover

02 Dec 2019 | all notes

Setting a background image to cover an entire element (often body or div) using CSS is easy:

body {
  background-image: url('image.jpg');
  background-size: cover;
}

Since the background image will be stretched to cover the entire element space, it should have a high enough resolution so that it does not appear pixellated on large screens. But, for the sake of mobile devices with smaller screens, it would be good to provide lower-resolution alternatives when screen resolution isn’t too high, to avoid making users download unnecessarily large files on potentially slow connections.

Assuming we want to put a responsive image as the background for the entire website, why don’t we just use CSS @media queries to select an appropriately-resized version of the background image?

body {
  background-image: url('image-original.jpg');
  background-size: cover;
}

@media only screen and (max-width: 2000px) {
  body {
    background-image: url('image-w2000.jpg');
  }
}

@media only screen and (max-width: 1000px) {
  body {
    background-image: url('image-w1000.jpg');
  }
}

Assuming our background image is in 4:3 format, this will work fine on landscape screens. But if we open the same website on a mobile device in portrait mode with a resolution of, say, 600x1200, the background will actually appear pixellated. Why? Well, based on the 600px screen width the media query will select the background image that is 1000x750px. We can immediately tell that this resolution is not high enough by the fact that the screen height exceeds the vertical resolution of the image by a factor of almost two.

But the same oversampling happens on the horizontal axis as well: not only does the background-size: cover property cause the background image to stretch out vertically in order to cover the full screen height, in order for the picture to keep the same aspect ratio a large part of the image will be cut off along the horizontal axis. In our hypothetical case, only a 375px wide strip of the image is shown, obviously not enough to cover the 600px screen width (which part exactly is cut off depends on the value of the background-position property, by default the left-most 375px will be visible).

How do we fix this? Well, we basically need to calculate what fraction of the image will actually be visible, and then increase the required image width so that the visible part has a resolution that’s at least as high as the screen space it needs to fill. I don’t think this can be achieved with CSS alone, so we will have to move on to using JavaScript here. Again for the case of a body background image, and assuming that the aspect ratio of our background pictures is 4:3, we can calculate the minimal image width necessary as:

window.innerWidth * Math.max(1, (4 * window.innerHeight ) / ( 3 * window.innerWidth));

You can use something like the following function to then set the CSS background-image property programatically from JavaScript:

let largestWidthLoaded = 0;
function setResponsiveBackgroundImage() {
	const minimumWidth = window.innerWidth * Math.max(1, (4 * window.innerHeight ) / ( 3 * window.innerWidth));

	// round to nearest available image resolution based on the
	// image sets you have available
	const responsiveWidth = ....

	if (responsiveWidth > largestWidthLoaded) {
		largestWidthLoaded = responsiveWidth;

		// create new filename based on responsiveWidth
		const bgImageFilename = ...
		// update CSS: set new background-image
		document.body.style.backgroundImage = bgImageFilename;
	}
}
setResponsiveBackgroundImage();

Finally we make sure that, in case the screen size is increased after the first page load, a higher resolution background is downloaded when necessary:

window.addEventListener('resize', setResponsiveBackgroundImage);

(Note that the effect of the resize event listener could probably also be achieved by dynamically calculating the screen width cutoffs based on the current aspect ratio and then setting up @media query event listeners in JavaScript.)

Comments