Northskull: Building a parallax scrolling system with vanilla Javascript

Having finished the job of building the front-end on the new Northskull website at Nation, I thought I would take a section of the site which was an interesting challenge, and explain how it went down - the β€œNorthskull Discover” page.

A screenshot of the "Northskull Discover" website from 2015.
A screenshot of the "Northskull Discover" website from 2015.

The parallax effect of the different elements on this page went through 3 main versions. You can see the final result on this page as you scroll around πŸ‘€ (except on mobile, sorry πŸ“ž)

Version 1 began with empty coloured blocks travelling up the page at a slightly slower speed than the users scroll speed. This achieved the technical side of the parallax effect, but wasn’t exactly, smooth.

Version 2 we started playing with CSS transitions on the translation that was happening to the element. This started to look a bit nicer, but was causing quite a lot of performance issues, mainly because there were so many blocks gliding around at once.

Version 3 was basically a clean slate. After a few different ideas with design, the code needed a bit of a scrub. Having figured out what did and didn't work, I focused on the scrolling performance by dividing the page up into β€˜containers’ and only scrolling the child elements when that container was inside the viewport. Here is how was that was achieved with code:

Obtain the top and bottom positions of each container:

for (var i = 0; i < CONTAINERS.length; i++) {
	var container = CONTAINERS[i];
	var rect = container.getBoundingClientRect();
	containerData.push({
		top: rect.top + window.pageYOffset - (document.documentElement.clientTop || 0),
		bottom: (rect.top + window.pageYOffset - (document.documentElement.clientTop || 0)) + container.clientHeight
	})
}

Loop containers and decide when to move the elements inside that container based on the each container position:

for (var j = 0; j < CONTAINERS.length; j++) {
	var container = CONTAINERS[j];
	var containerTop = containerData[j].top;
	var containerBottom = containerData[j].bottom;
			
	// If this container is inside the viewport, move the items inside
	if (containerTop < windowBottom && containerBottom > windowScrollTop) {
		// Get the progress of how far the current container has travelled up the viewport.
		// Number from 0 to 100.
		var distance = (windowScrollTop + window.innerHeight) - containerTop;
		var progress = Math.round(distance / ((window.innerHeight + container.clientHeight) / 100));
		adjustParallaxElements(j, progress);
	}
}

Finally, move the child elements inside the container that is within the viewport:

function adjustParallaxElements(containerID, progress) {
	var container = CONTAINERS[containerID];
	var item = container.querySelectorAll('article');
	for (var i = 0; i < item.length; i++) {
		var newTop = (progress / 100) * item[i].getAttribute('data-percentage-to-travel');
		var value = 'translateY(-'+newTop+'%)';
		item[i].style.webkitTransform = value;
		item[i].style.msTransform = value;
		item[i].style.transform = value;
	}
}

As you can see the translateY of the child element is being adjusted. To add the smoothness we were looking for previously, we added a 400ms transition to each element to mimic our inertia.

Oh yeah, each element has a data-attribute called data-percentage-to-travel. Nice naming, I know. This defines how far the element travels within it's parent container πŸ’ƒ

There are a million (roughly) ways to do everything with front-end so if you want to pull my idea apart and tell me I am an idiot, I'm right here – @tomsansome πŸ‘‹πŸ™‚