Scrollama.js 滚动步骤导航切换工具

1,904 阅读3分钟
原文链接: pudding.cool

What

Scrollama is a modern and lightweight JavaScript library for scrollytelling using IntersectionObserver in favor of scroll events. Using IntersectionObserver abstracts element-in-view detection and removes the need to monitor scroll events, which contribute towards a sluggish experience. It offers three main features:

  • Step triggers: Fires an event when an element crosses a visible threshold.
  • Step progress: Fires events more granularly from 0-100% completion of a step.
  • Sticky graphic: Convenience functions for the opinionated (but popular) scrollytelling sticky graphic pattern.

It is vanilla JS so there are no dependencies like jQuery or D3. Although IntersectionObserver is a bleeding edge technology, Scrollama is polyfilled for browsers without support yet.

Why

Scrollytelling can be complicated to implement and difficult to make performant. The goal of this library is to provide a simple interface for creating scroll-driven interactives and improve user experience by reducing scroll jank. It offers (optional) methods to implement the common scrollytelling pattern to reduce more involved DOM calculations. For lack of a better term, I refer to it as the sticky graphic pattern, whereby the graphic scrolls into view, becomes "stuck" for a duration of steps, then exits and "unsticks" when the steps conclude.

How

Enough talk, time for the good stuff. While you can take a look at the documentation for basic usage, the remainder of this post will be a step-by-step guide to putting together an entire scrollytelling story with scrollama, including the HTML and CSS. Please note that this is a barebones, desktop-centric implementation, please read my post on responsive scrollytelling best practices for things to consider for mobile.

HTML

<section id='scroll'>
	<!--  graphic container  -->
	<div class='scroll__graphic'>
		<!--   actual graphic/chart   -->
		<div class='chart'>
			<p>0</p>
		</div>
	</div>

	<!--  step/text container  -->
	<div class='scroll__text'>
		<div class='step' data-step='1'>
			<p>STEP 1</p>
		</div>
		<div class='step' data-step='2'>
			<p>STEP 2</p>
		</div>
		<div class='step' data-step='3'>
			<p>STEP 3</p>
		</div>
		<div class='step' data-step='4'>
			<p>STEP 4</p>
		</div>
	</div>
</section>

Inside of #scroll there are two main elements, the graphic container .scroll__graphic and the text container .scroll__text. The graphic here is a container for the actual visual presentation (chart). This is useful for positioning non-full screen elements as you will see shortly. The text is simply a container for each step element .step. The step elements are used to trigger the graphic to change.

I like to put the data attribute data-step with information that tells the JS how to change the graphic when it is triggered. This could be mapped to an array or object in your JS with more detailed data or actions.

CSS

#scroll {
  position: relative;
}

Relatively positioning the parent container is important so that anything that any absolutely positioned children will be relative to where the parent is, not the body.

.scroll__graphic {
  position: absolute;
  top: 0;
  left: 0;
  bottom: auto;
  width: 100%;
  -webkit-transform: translate3d(0, 0, 0);
  -moz-transform: translate3d(0, 0, 0);
  transform: translate3d(0, 0, 0);
}

.scroll__graphic.is-fixed {
  position: fixed;
}

.scroll__graphic.is-bottom {
  bottom: 0;
  top: auto;
}

The graphic container is absolutely positioned, so it will be at the very top of the parent container to start. The height will be set in JS to be 100% of the viewport. Adding a 3D transform reduces some janky jump stuff when going from fixed to absolute position. We add the class .is-fixed in the JS when the should stick, and .is-bottom When the graphic should unstick and lock into place when we scroll passed the section.

.scroll__graphic .chart {
  position: absolute;
  right: 1rem;
  top: 50%;
  -moz-transform: translateY(-50%);
  -webkit-transform: translateY(-50%);
  transform: translateY(-50%);
}

Although you can put whatever you want inside the graphic container, here is a common way to add a non-fullscreen, vertically-centered chart to the mix. The width and height are set in JS.

There is no necessary CSS for the text container and the steps. The layout can take on any size, or spacing you prefer. Here is a basic left-aligned approach for an example.

.scroll__text {
  padding: 0 1rem;
  max-width: 30rem;
}

.step {
  opacity: 0;
  padding: 1rem;
}

.step.is-active {
  opacity: 1;
}

JS

Okay, now we can get into the guts of it and implement scrollama. Here is a top-level look at the structure. Then we will break down what goes on inside each function.

// using d3 for convenience, and storing a selected elements
var $container = d3.select('#scroll');
var $graphic = container.select('.scroll__graphic');
var $chart = graphic.select('.chart');
var $text = container.select('.scroll__text');
var $step = text.selectAll('.step');

// initialize the scrollama
var scroller = scrollama();

// resize function to set dimensions on load and on page resize
function handleResize() { ... }

// scrollama event handlers
function handleStepEnter(response) { ... }

function handleContainerEnter(response) { ... }

function handleContainerExit(response) { ... }

// kick-off code to run once on load
function init() {...}

// start it up
init();

The structure should be pretty straight forward. There are a few "global" variables, a function to handle resize, a few functions to handle the scrollama callback events, and a function to set things up.

function init() {
	// 1. call a resize on load to update width/height/position of elements
	handleResize();

	// 2. setup the scrollama instance
	// 3. bind scrollama event handlers (this can be chained like below)
	scroller
		.setup({
			container: '#scroll', // our outermost scrollytelling element
			graphic: '.scroll__graphic', // the graphic
			text: '.scroll__text', // the step container
			step: '.scroll__text .step', // the step elements
			offset: 0.5, // set the trigger to be 1/2 way down screen
			debug: true, // display the trigger offset for testing
		})
		.onStepEnter(handleStepEnter)
		.onContainerEnter(handleContainerEnter)
		.onContainerExit(handleContainerExit);

	// setup resize event
	window.addEventListener('resize', handleResize);
}

Inside the init function, we force a resize to update dom elements, then we initialize the scrollama instance. The only required option is the step paramenter. The sticky graphic pattern however requires the other three dom elements.

function handleResize() {
	// 1. update height of step elements for breathing room between steps
	var stepHeight = Math.floor(window.innerHeight * 0.75);
	$step.style('height', stepHeight + 'px');

	// 2. update height of graphic element
	var bodyWidth = d3.select('body').node().offsetWidth;

	$graphic
		.style('height', window.innerHeight + 'px');

	// 3. update width of chart by subtracting from text width
	var chartMargin = 32;
	var textWidth = $text.node().offsetWidth;
	var chartWidth = $graphic.node().offsetWidth - textWidth - chartMargin;
	// make the height 1/2 of viewport
	var chartHeight = Math.floor(window.innerHeight / 2);

	$chart
		.style('width', chartWidth + 'px')
		.style('height', chartHeight + 'px');

	// 4. tell scrollama to update new element dimensions
	scroller.resize();
}

The main thing to note here is that this is where you can start highly customizing your presentation and layout of the graphics and charts. For example, you don't need to set the height of the steps via JS, you could simply add more margin on the elements.

function handleStepEnter(response) {
	// response = { element, direction, index }

	// fade in current step
	$step.classed('is-active', function (d, i) {
		return i === response.index;
	})

	// update graphic based on step here
	var stepData = $step.attr('data-step')
	...
}

function handleContainerEnter(response) {
	// response = { direction }

	// sticky the graphic
	$graphic.classed('is-fixed', true);
	$graphic.classed('is-bottom', false);
}

function handleContainerExit(response) {
	// response = { direction }

	// un-sticky the graphic, and pin to top/bottom of container
	$graphic.classed('is-fixed', false);
	$graphic.classed('is-bottom', response.direction === 'down');
}

The last piece of the puzzle is the scrollama callback functions. In the first one, we can trigger changes to the chart by handling which step becomes active. In the enter and exit callback functions, we stick and unstick the graphic, respectively.

And there you have it! You can see this in action and take a look at the entire code. There are also many different ways to approach this pattern. I have documented a few examples you can check, including other ways to use scrollama. Be sure to read the full documentation!