Mayank Mandava

Placing a background image on a scrolling page

I wanted a background image on this blog (shout out to unsplash-logoMax Andrey for my current lovely background):

body {
  background-image: url(/static/sea.jpg);
}

But I wasn't sure how to deal with scrolling.

I didn't want to scale the image vertically, since that destroys the aspect ratio:

background-repeat: no-repeat;
background-size: 100% 100%;
background-attachment: local;
1
2
3

And I didn't want to repeat the image, since that just looks terrible:

background-repeat: repeat;
background-size: 100%;
background-attachment: local;
1
2
3

One easy solution is to just set your background-attachment to scroll (or fixed if it's the body background). Read more about it here. This makes the image fixed wrt to the parent element:

background-repeat: no-repeat;
background-size: 100%;
background-attachment: scroll;
1
2
3

But I didn't love this either because it doesn't let me show off the whole image. I wanted the background image to scroll at the same percentage as the content, and not the same number of pixels. That means, at the end of the content, you are also at the end of the backgroud image, like this:

1
2
3

My CSS knowledge extends to reading MDN pages and throwing things at people, so I coulnd't figure it out. But it was easy enough to do in javascript. The basic idea being:

  1. Listen to the scroll event on the parent element
  2. Find out how much the content is scrolled in percentage terms
  3. Set the background position at the same percentage
const scrollTopMax = el.scrollHeight - el.clientHeight;
el.onscroll = () => {
  const scrollTopPercentage = Math.min(1.0, el.scrollTop / scrollTopMax);
  el.style.backgroundPositionY = `${scrollTopPercentage * 100}%`;
};

I threw in the Math.min to account for some fuzzing that happens in these properties (most likely due to padding).

For the body background, just replace el with document.scrollingElement, and set the style on document.body

const el = document.scrollingElement;
const scrollTopMax = el.scrollHeight - el.clientHeight;
document.body.onscroll = () => {
  const scrollTopPercentage = Math.min(1.0, el.scrollTop / scrollTopMax);
  document.body.style.backgroundPositionY = `${scrollTopPercentage * 100}%`;
};

And that is exactly what this page does.

Update (2019-07-29)

Based on a suggestion by reddit user howdoigetauniquename, it turns out that using a transform is much smoother than setting the background position. It does involve moving the background to a new div

#background {
  position: fixed;
  background-image: url("/static/sea.jpg");
  width: 100%;
  height: 150%;
  background-size: 100% 100%;
  background-position: 0% 0%;
  z-index: -1;
}

I've set the height of the div to be 150% of the width since that's the aspect ratio of our image. We want to move this 33.33% up at the bottom of the scroll to see the full picture.

const backgroundEl = document.getElementbyId("background");
const el = document.scrollingElement;
const scrollTopMax = el.scrollHeight - el.clientHeight;
document.body.onscroll = () => {
  const scrollTopPercentage = Math.min(1.0, el.scrollTop / scrollTopMax);
  const translate = 33.33 * scrollTopPercentage;
  backgroundEl.style.transform = `translate(0%,-${translate}%)`;
};