Building Better Backgrounds

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

Zack Krida
Jul 17, 2017 · 16 min read

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 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 , 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 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 the mobile design. Maybe the mobile version doesn’t need to match the functionality of the desktop version, but should instead 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 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 on large displays. Or:
  2. We need to make a behave like an <img> tag on small 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 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 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 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 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? 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 , 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 , 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 , 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 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 Specifically, the space needs to be 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 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 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 by using the window.getComputedStyle method in JavaScript. To get , 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 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 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:

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 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 . 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 This will boost performance significantly. I recently wrote what is the

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 ; 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!If you’re curious, you can view this JavaScript technique in production here: https://www.shleppers.com/.

Zack Krida

Written by

person in providence