Building Better Backgrounds

Approaches to stacking css background images on mobile for improved readability, using object-fit and ES2015+.

Imagine you work at a small web firm and your designer just sent you mockups for a new marketing site (Static mockups, designer? Get with the times!). She asks you to design a homepage with the following content sections:

Simple, right? Just a css background image; some padding and margin around; and a background color on the content. You jump into your code editor, grab a photo from your wedding (because stock photo licenses are confusing), and come up with something up like this:

An example section with a CSS background image
<div class="c-section">
<div class="c-section__content">
<h1>Section Heading</h1>
<p>Lorem...</p>
<button>Call to Action</button>
</div>
</div>
<style>
/* Apply a background image to the section,
position it so we can see the couple,
and set the background to cover the available space */
.c-section {
max-width: 1400px;
margin: 0 auto;
padding: 48px;
background-image: url('..image.jpg');
background-size: cover;
background-position: right top;
}
/* Pad the content, make it 50% wide, and 
add the transparent background color */
.c-section__content {
padding: 24px;
width: 50%;
background-color: rgba(255, 255, 255, .4);
}
</style>

Looks great, right? Just what the designer ordered. After shipping the site with your new sections, you realize something: the designer never mocked up mobile. D’oh!

The sections you launched need to be made responsive. The question arises — how should they look at the smallest breakpoint? You’re a self reliant developer, and you feel embarrassed to ask you designer for yet another mockup. You quickly try out a couple of styles, using the same code, but making the content full-width and playing with color:

…yuck.

These aren’t terrible, but they certainly aren’t good! We completely obscure our subject’s faces; the original composition of the image is lost; and the text legibility is significantly reduced. Additionally, things start looking even worse once we add a section with longer content. We even lose one of our subjects entirely!

this poor man looks so lonely…

What we really need here is to completely rethink the mobile design. Maybe the mobile version doesn’t need to match the functionality of the desktop version, but should instead present the content in an ideal way. You decide to loop the designer back in after all, and she provides you with a much more sensible small-device mockup:

i can read it now!

This looks much better. Our content is legible and our image retains not only it’s original composition, but we can clearly see the subjects’ faces. It looks great! On large displays we’ll continue to show the photo as a background image, but on smaller ones we’ll stack the content below the image. The only question is: how on earth do we code this?


Our goal here seems relatively straightforward, but we have to remember we’re talking about CSS here. Remember vertical-centering pre-flexbox?

Anyway, if you look at the newest mobile mockup and think about it in it’s simplest form, it’s really just an html image tag followed by some padded content. We could express it like this very simply:

<div class="c-section">
<img class="c-section__image" src="../image.jpg">
<div class="c-section__content">
<h2>Section Title</h2>
<p>Lorem ipsum dolor...</p>
<button>Call to Action</button>
</div>
</div>
.c-section__image {
width: 100%;
height: auto;
}
.c-section__content {
padding: 24px;
}

With our previous implementation on large displays using css background images and our new mobile approach using image tags, the solution to our problem can be explained in two ways:

  1. We need to make an <img> behave like a background-image on large displays. Or:
  2. We need to make a background-image behave like an <img> tag on large displays.

So, let’s get started! Each approach has it’s pros and cons, which I’ll do my best to outline.


Making an <img> tag behave like a background-image

our final result

To make this happen we’re going to have do a few things. We’ll start with an image tag above the content just like in our most-recent code sample:

<div class="c-section">
<img class="c-section__image" src="../image.jpg">
<div class="c-section__content">
<h2>Section Title</h2>
<p>Lorem ipsum dolor...</p>
<button>Call to Action</button>
</div>
</div>
.c-section__image {
width: 100%;
height: auto;
}
.c-section__content {
padding: 24px;
}

Now, we’ve already achieved the desired effect on small displays. To make large displays work we’re going to:

  1. Absolutely position the .c-section__image to its parent .c-section. We’ll have to add position: relative to the .c-section to give the image a positioning context.
  2. Make the .c-section__content 50% wide again, and re-add the transparent background.
  3. Add overflow: hidden to the .c-section so that the image doesn’t overlap outside of the section.
  4. Give the .c-section__content a positioning context and a z-index so it shows up above the image.

Our updated code will look something like this:

<div class="c-section">
<img class="c-section__image" src="../image.jpg">
<div class="c-section__content">
<h2>Section Title</h2>
<p>Lorem ipsum dolor...</p>
<button>Call to Action</button>
</div>
</div>
.c-section {
/* This makes sure the image doesn't stick outside of the
section */
position: relative;
overflow: hidden;
}
.c-section__image {
/* The image will fill the width of the section and preserve
it's aspect ratio */

width: 100%;
height: auto;
position: absolute;
left: 0;
top: 0;

}
.c-section__content {
padding: 24px;
width: 50%;
/* This makes sure the content is above the image */
position: relative;
z-index: 2;

}

and our finished result like this:

looks good!

Great! that’s exactly what we wanted. Finally, we can add some media queries to seamlessly switch between the two styles. Our final css code looks like this:

.c-section {
position: relative;
overflow: hidden;
}
.c-section__image {
width: 100%;
height: auto;
}
@media (min-width: 600px) {
.c-section__image {
position: absolute;
left: 0;
top: 0;
}
}
.c-section__content {
width: 100%;
padding: 24px;
position: relative;
z-index: 2;
}
@media (min-width: 600px) {
.c-section__content {
margin: 24px;
background-color: rgba(255, 255, 255, .5);
width: 50%;
}
}

Perfect! Once again we get the desired effect:

still looks great…right?

One problem with this approach occurs when our content is taller than the background image. Currently, it will look like this:

gross.

The image stays 100% wide, To fix this, we can use the object-fit and object-position properties. We’ll update the .c-section__image css to look like this:

.c-section__image {
width: 100%;
height: 100%;
object-fit: cover;
object-position: top center;

}

Does this syntax look familiar? It should! The object properties work just like the background-image and background-position properties, but for HTML images and videos.

With out new code, the long-content version looks as-expected:

perfect!

Finally, we are done with the image as background-image approach. Some of the benefits of this approach are:

  • Can be used with HTML responsive images (the srcset and sizes properties) to decrease page size.
  • Improved SEO and accessibility: Background images can have alt text
  • No JavaScript required

There are also some cons to consider:

  • object-fit is supported in all modern browsers, but not in Internet Explorer, Edge, or older versions of Android & Safari: http://caniuse.com/#feat=object-fit
  • When object-fit is unsupported, images appear as stretched and distorted
  • Your background images are now part of your content, and not just a decorative flair. This has implications for SEO, search indexing, and screen readers.

In conclusion, the image as background-image approach works well if you don’t need to support windows-specific browsers.

Note: There is a polyfill for object-fit that will give you full support in IE9+. It just requires a bit of trickery and configuration.


Making a background-image behave like an <img> tag

For this approach, there’s no css property to make a css background-image display inline like an image. We’re going to have to use JavaScript to position our image where we want it! If you haven’t written much JavaScript — you should start! It’s a fun and powerful way to add functionality to your site. We’re going to write our JavaScript using ES2015+ syntax, the latest standard of the language. To learn more about ES2015-ES2017 and more check out this presentation.

Let’s begin by comparing our original mobile design to the one from our previous example:

In the first example, our image is cropped and obscured. In the final implementation it stays true to the original.

In our first attempt, we crop the image so it covers all the available height. It seems like a good starting point for our background-image approach to preserve the aspect ratio of the background image. Once we get the image the correct size, then we can work on positioning it. Let’s give that a try.

We’re going to start this approach with similar code to our original example, but this time, we’re going to remove background-size: cover on mobile. Instead, we’re going to make the background image 100% wide and give it an automatic height to preserve it’s original aspect ratio, just like in our image-tag-to-background-image example. The code ends up looking like this:

.c-section {
padding: 24px;
background-image: url(https://www.dropbox.com/s/xm1fohp06rfh1zd/2337770b75e90edc80cbe42d34c3c2bd-xxlarge.jpg?dl=1);
background-size: 100% auto;
background-repeat: no-repeat;

}
@media (min-width: 600px) {
.c-section {
background-size: cover;
background-position: top right;
}
}
.c-section__content {
width: 100%;
padding: 24px;
position: relative;
z-index: 2;
}
@media (min-width: 600px) {
.c-section__content {
background-color: rgba(255, 255, 255, .5);
width: 50%;
}
}

and our example looks like this on mobile:

so close!

Already we’ve got a few things right:

  • Our image is the right proportions
  • Our content is padded correctly

We really just need to find a way to add more space above the content. Specifically, the space needs to be the height of the image. We could manually add a padding-top to the content, but this approach fails quickly. As soon as we resize the browser, our padding-top would no longer match the new dimensions of the image. What we really need is a way to dynamically set the padding-top of the content based on the current height of the image. Whenever your site needs dynamic values, it’s usually JavaScript to the rescue!

For simplicity’s sake, pretend our JavaScript is in either an included app.js file or an inline <script> tag positioned before the closing</body> tag on our page. We’re going to start by creating variables for our sections. Since our page might have multiple .c-sections, we’re going to write code that will correctly resize all of the background images. We’ll start by selecting all of our sections:

// Store all of our .c-sections in a variable called 'sections'
const sections = document.getElementsByClassName('c-section');

Now we have all of our sections as a list in one variable. Great! However, you may assume that this list is a JavaScript Array, but document.getElementsByClassName actually returns what’s known as an HTML Collection. We need to turn this collection into an array, which ES2015’s Array.from(...array) method makes simple! You can pass Array.from an HTML Collection, Node List, or other array-like structure and turn it into an array. Our list of sections now looks like this:

// Store all of our .c-sections in a variable called 'sections'
const sections = Array.from(document.getElementsByClassName('c-section'));

As we said previously, we want to run our code for each section, so we’re going to loop through our array using the forEach method, like

// Store all of our .c-sections in a variable called 'sections'
const sections = Array.from(document.getElementsByClassName('c-section'));
sections.forEach(section => {
// do stuff in here
});

Great! Now we’re ready to write our logic to grab the background image from each section, detect its height, and apply that height as padding-top to the section. To do this, JavaScript needs to know the dimensions of our image. We’ll need to get the image url from the background-image css property on our .c-section. We can get the values of all css properties on an element by using the window.getComputedStyle method in JavaScript. To get the value of a single css property, we can follow our call of getComputedStyle with a call togetPropertyValue('background-image'). The whole thing ends up looking like this:

// Store all of our .c-sections in a variable called 'sections'
const sections = Array.from(document.getElementsByClassName('c-section'));
sections.forEach(section => {
// Assign the background-image value to the variable 'image'
const imageUrl = window
.getComputedStyle(section)
.
getPropertyValue('background-image')
});

If we run this JavaScript on our section, the value of the imageUrl variable will be url(“https://www.dropbox.com/s/xm1fohp06rfh1zd/2337770b75e90edc80cbe42d34c3c2bd-xxlarge.jpg?dl=1"). This is close to what we want, but look at the output. The permalink to the image is wrapped in the url(" ") text required by CSS. We could use JavaScript’s string.replace() method to replace the url(“ and ") pieces. This would look like this:

// Store all of our .c-sections in a variable called 'sections'
const sections = Array.from(document.getElementsByClassName('c-section'));
sections.forEach(section => {
// Assign the background-image value to the variable 'image'
const imageUrl = window
.getComputedStyle(section)
.getPropertyValue('background-image')
// Remove 'url("' and ')' from the image url string
.replace('url("', '')
.replace('")', '')

});

This works but there’s another problem. In css, you can write url() with double quotes, like above, but also with single quotes, or even no quotes! All of the following are valid syntax:

  • background-image: url(http://image.com)
  • background-image: url('http://image.com)'
  • background-image: url("http://image.com")

To support these three cases, we’ll follow some simple steps:

  1. Get the string between the first 4 characters:
    url( and the last character ).
  2. Remove every single or double quote in the url

Will handle this with JavaScript’s slice and replace methods. Our final JavaScript for this will look as follows:

// Store all of our .c-sections in a variable called 'sections'
const sections = Array.from(document.getElementsByClassName('c-section'));
sections.forEach(section => {
// Assign the background-image value to the variable 'image'
const imageUrl = window
.getComputedStyle(section)
.getPropertyValue('background-image')
// Get the piece of the string starting after the 4th
character and ending with the 2nd to last character.
// Then, replace all occurrences of ' or " with a blank
string.
.slice(4, -1).replace(/["|']/g, "")

});

Awesome. now the value of image is our url. Phew!


At this point, we have the url of our background image for each section. Now, we need to get the dimensions of each image! In JavaScript we will need an HTMLImageElement for each url, which we can then get the width and height off of. To create a new HTMLImageElement without attaching it to the DOM (i.e, adding it to our HTML page), we can use the new Image() constructor. For each .c-section we’ll create an HTMLImageElement and attach our imageUrl as the source. All together it looks like this:

// Store all of our .c-sections in a variable called 'sections'
const sections = Array.from(document.getElementsByClassName('c-section'));
sections.forEach(section => {
// Assign the background-image value to the variable 'image'
const image = new Image();
const imageUrl = window
.getComputedStyle(section)
.getPropertyValue(‘background-image’)
// Get the piece of the string starting after the 4th
character and ending with the 2nd to last character.
// Then, replace all occurrences of ' or " with a blank
string.
.slice(4, -1).replace(/["|']/g, "")

// Make the imageUrl the .src of the image
image.src = imageUrl;

});

Finally, we can access the height and width of the image with image.height and image.width, respectively. We can now set our section’s padding-top to the height of the image!

// Store all of our .c-sections in a variable called 'sections'
const sections = Array.from(document.getElementsByClassName('c-section'));
sections.forEach(section => {
// Assign the background-image value to the variable 'image'
const image = new Image();
const imageUrl = window
.getComputedStyle(section)
.getPropertyValue(‘background-image’)
// Get the piece of the string starting after the 4th
character and ending with the 2nd to last character.
// Then, replace all occurrences of ' or " with a blank
string.
.slice(4, -1).replace(/["|']/g, "")

// Make the imageUrl the .src of the image
image.src = imageUrl;

// Set the padding-top, woohoo! Finally.
section.style.paddingTop = image.height + 'px';

});

I hope I didn’t lead you on, because when you run this code, it doesn’t work! That’s because when this JavaScript executes, the browser may not have finished loading the images yet. We need to wrap our code in a event that fires once we know the images are loaded. The window object’s load event is a perfect candidate. MDN describes it as follows:

The load event fires at the end of the document loading process. At this point, all of the objects in the document are in the DOM, and all the images, scripts, links and sub-frames have finished loading.

This sounds like exactly what we need, so we’ll now wrap our code in an event listener:

// Store all of our .c-sections in a variable called 'sections'
const sections = Array.from(document.getElementsByClassName('c-section'));
window.addEventListener('load', () => {
sections.forEach(section => {
// Assign the background-image value to the variable 'image'
const image = new Image();
const imageUrl = window
.getComputedStyle(section)
.getPropertyValue(‘background-image’)
// Get the piece of the string starting after the 4th
character and ending with the 2nd to last character.
// Then, replace all occurrences of ' or " with a blank
string.
.slice(4, -1).replace(/["|']/g, "")

// Make the imageUrl the .src of the image
image.src = imageUrl;

// Set the padding-top, woohoo! Finally.
section.style.paddingTop = image.height + 'px';
});
});

This is ugly, so now might be a good time to refactor. Look at how many layers of nesting we have. We can store our section.forEach call inside a function in case we need to run it multiple times. At this point, our code is explicit enough that we can remove our comments as well. We can also just set image.src without first storing it as imgUrl— since we’re only using it once it really doesn’t need its own variable.

Finally, we also want our code to run any time we resize the browser! There’s another window event resize that we can write the same way as load. The cleaned up version looks like this:

const sections = Array.from(document.getElementsByClassName('c-section'));
const sectionBgStack = () =>
sections.forEach(section => {
const image = new Image();
image.src = window
.getComputedStyle(section)
.getPropertyValue(‘background-image’)
.slice(4, -1).replace(/["|']/g, "")

section.style.paddingTop = image.height + 'px';
});
}
// Run 'sectionBgStack' on window load and resize
window.addEventListener('load', sectionBgStack);
window.addEventListener('resize', sectionBgStack);

This looks a lot cleaner, and if you run it, it works! Wait a second, what’s all that white space?!

oh no!

If you inspect your section, you’ll see our code is adding a whopping 1067px of padding-top to our section! Wow! Where’s that 1067 number coming from?

Well, if you download the image we’re using, you can see that it just so happens to be 1067px tall.

there it is…1067!

What’s happening is that our call to image.height is retrieving the height of the actual image file, not the height the image is displaying at. There is no such property on the image object, so we’ll have to calculate it ourselves.

Basically, we need the height of the image based on how wide the image is displaying. So in this case, if the image was showing half as wide, (800px), we’d want the height to be halved as well (533.5px). To do this we’ll base the height value off of the width value. But how? Like image.height, image.width returns the value of the image file, not the size it is displaying at. So, what can we get calculate the width of the image off of? Is there another element with the same width as the image?

The section!

That’s right, our image is the same width as our .c-section! And since we’re already looping through our sections, we have the section width available to us. If we divide the image.width by the .c-section width, we can get a scale ratio. If we multiply the image.height by our scale ratio, we’ll have the correct size! Our revised code:

const sections = Array.from(document.getElementsByClassName('c-section'));
const sectionBgStack = () => {
sections.forEach(section => {
const image = new Image();
image.src = window
.getComputedStyle(section)
.getPropertyValue('background-image')
.slice(4, -1).replace(/["|']/g, "")

const scale = section.offsetWidth / image.width;
section.style.paddingTop = (image.height * scale) + 'px';
})
};
// Run 'sectionBgStack' on window load and resize
window.addEventListener('load', sectionBgStack);
window.addEventListener('resize', sectionBgStack);

Finally, that ‘resize’ event is a problem. It fires off as many calls as possible on window resize and is terrible for performance! We’ll want to throttle our resize call so it can only happen once per 300ms. This will boost performance significantly. I recently wrote what is the simplest, smallest throttle function i’ve ever seen in the wild:

So, let’s add that to our project and throttle our call every 300ms:

const throttle = (func, limit) => {
let wait = false;
return () => {
if (wait) return;
func();
wait = true;
setTimeout(() => wait = false, limit);
}
}
const sections = Array.from(document.getElementsByClassName('c-section'));
const sectionBgStack = () => {
sections.forEach(section => {
const image = new Image();
image.src = window
.getComputedStyle(section)
.getPropertyValue('background-image')
.slice(4, -1).replace(/["|']/g, "")

const scale = section.offsetWidth / image.width;
section.style.paddingTop = (image.height * scale) + 'px';
})
};
// Run 'sectionBgStack' on window load and resize
window.addEventListener('load', sectionBgStack);
window.addEventListener('resize', throttle(sectionBgStack, 400));

And with that, we’re actually done. Whew! Here’s the pros of this approach:

  • It got you to write some cool JavaScript, right? 2017 baby.
  • Your images aren’t part of your content; they are merely decoration.

And the cons:

  • Your JavaScript will need to be run through a build process (think Babel, Buble, or other tools) for compatibility with older browsers.
  • There’s probably performance considerations from new Image()-ing so many times, if your site has a large number of sections.

Well, I tried. Hopefully you learned something at least!