Parallaxify

A JavaScript module to easily produce a parallax image effect.
September 1, 2015 · See the code

Motivation

While building a microsite for a family member I wanted to include an elegant parallax image effect on scroll. A parallax effect is when an image appears to scroll slightly faster than the rest of the page, producing a subtle shifting/drifting sensation to convey depth.

I wasn't able to find a simple, reusable solution so I created my own and published it for anyone to use. The code is super-lightweight and requires zero dependencies.

Lessons and improvements

I hadn't touched the code in years, but I revisited it for this writeup and realized how much I've learned since the original publication. I was able to reduce the lines of codes by 65% (from 55 to 23 lines). The minified version shrunk 59% (from 932 to 374 Bytes).

A diff of the before/after can be found on Github, but I'll share a few key improvements below.

Pure functions

Instead of defining percentSeen inside the for loop and referencing an external variable (element), define percentSeen once and accept an argument (el).

Before
for (let i = 0; i < elements.length; i++) {
 let element = elements[i]
 let elementOffsetTop = element.offsetTop

 function percentSeen () {
   let distance = (winScrollTop + viewportHeight) - elementOffsetTop
   ...
 }
}
After
function getPercentSeen(el) {
 let distance = window.scrollY + window.innerHeight - el.offsetTop
 ...
}

for (let i = 0; i < elements.length; i++) {
   const el = elements[i];
   el.style.backgroundPositionY = `${getPercentSeen(el)}%`;
   ...
}

Improved clamping

Instead of the naive if (someTrueThing) return true; else return false pattern, I return a clamped number based directly on the source value.

Before
if (percentage < 0) return 0;
else if (percentage > 100) return 100;
else return percentage;
After
return Math.min(Math.max(percentage, 0), 100); // clamp between 0 and 100

Reference values directly

Previously, I jumped through hoops to define and maintain the viewportHeight variable when the script was initialized or the window was resized. A simpler approach is to just reference the underlying window.innerHeight value directly as-needed, and not worry at all about syncing another variable. If the window is resized, the up-to-date value is used.

Before
let viewportHeight = 0

let updateWindowCalcs = function () {
  viewportHeight = window.innerHeight
}

updateWindowCalcs()

window.addEventListener('resize', function(){
  updateWindowCalcs()
})

function percentSeen () {
  ...
  let distance = (winScrollTop + viewportHeight) - elementOffsetTop
  ...
}
After
function getPercentSeen(el) {
  let distance = window.scrollY + window.innerHeight - el.offsetTop;
  ...
}

Function naming

Use naming conventions to indicate if a function is getting or setting a value.

Before
function percentSeen () {...}

element.style.backgroundPositionY = percentSeen()+'%'
After
function getPercentSeen(el) {...}

el.style.backgroundPositionY = `${getPercentSeen(el)}%`;