Introduction section
For awhile I’ve wanted to use scrollytelling to better present data visualisation. This is a very well established technique in data journalism and has been used in publications like The New York Times. This is all great in theory, but in practice I’ve already created my (static) website and blog using Quarto and so I had to work out how this could be nicely integrating into my blog. In this post I’ll show how I incorporated interactive scrolling in a Quarto blog post using the scrollama
JS library. Then, in future posts, I’ll demonstrate how we can combine that with D3 to create scrollytelling visuals.
This is written with the expectation that you have a introductory understanding of HTML and JavaScript. If you need to learn more, something like W3 schools can provide a nice basic introduction.
Scrollama library
To incorporate scrollytelling we’ll be using scrollama.js, which was created by Russell Samora at The Pudding. The Pudding already has some beautiful examples of scrollytelling to get inspired (see this NBA article). This library uses intersection observer so that scroll events will be triggered when our page intersects with a particular HTML element.
Obviously this will involve us working in JavaScript. For larger projects, I would suggest to use a separate .js file(s); however, to provide an easy, stand-alone example, this time we’ll just add JS code inside our Quarto doc within a <script>
HTML element.
Simple scrollama: Make our sections appear as we scroll
You might have already noticed that each section on the page is fading in as you scroll. How do we achieve something like this with scrollama? The most basic scrollama project will involve 3 steps:
- Setup our workspace
- Define what happens on a scroll event
- Setup and initialise a scrollama object
We’ll work through each of these steps below and then bring it all together.
Setup our workspace
We will use 2 libraries in this example:
- scrollama.js, to define what happens when our page intersects with an HTML element.
- d3.js, to select and manipulate HTML elements. NOTE: We could use vanilla JS for manipulating HTML elements, but we’ll be using D3 for other tasks in the future.
We can load each library easily by adding an HTML <script>
element and specifying the source (URL) where these libraries are found. Both libraries are quite well developed and stable, so I expect these URLs to be suitable for awhile. If you’re coming to this blog in the future you might need to check for newer versions and source URLs. Note also that there is a library for the same process using React, which might be more relevant in the future.
<script src="https://unpkg.com/scrollama"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
In this simple example we want to make elements ‘appear’ as we scroll over them. This means that these objects need to be ‘invisible’ (i.e. opacity 0) to start. We can do this using D3. Don’t worry if you don’t understand all the D3 code right now, as we’ll discuss this in future blog posts.
// Select all HTML elements with class 'level2'
.selectAll(".level2")
d3// Select all children elements
.selectChildren()
// Make them 'invisible' with 0 opacity
.style("opacity", 0)
When we convert Quarto to HTML, each title is converted into a <section>
HTML element with class ‘levelX’ where X is the level of header. In our example, we’ve used 2nd level headers (i.e. ## TITLE in Markdown) so each of our sections will have class ‘level2’.
Define what happens on a scroll event
Scrollama allows us to trigger a piece of JavaScript code when our page intersects with an HTML element, such as a <h1>
title or <p>
paragraph element. This code can be whatever you want, but often we’ll want to change the attribute or style of the element that we intersect with. In our example, we’ll be changing the opacity of each element we intersect with.
The function we create should have an argument where information of the intersected event will occur. In our code below, we’ve called that argument intersected
. Our intersected
argument will include 3 pieces of information:
- The HTML element being intersected with
- The direction at which the intersection occurred (i.e. are we scrolling down or up when the intersection occurred?)
- The index of the intersection event. You’ll see when we setup our scrollama object that we specify which types of elements will be flagged
In this simple example we’re not using the direction
or index
information, we just focus on the intersected element. In the below code chunk, we take our all children elements within our intersected element and make it visible (i.e. opacity 1). We use .transition()
and .duration()
to make the transition smooth.
// Create scrollama event handlers
// intersected = { element, direction, index }
const handleStepEnter = (intersected) => {
// Convert HTML element into a D3 selection object...
.select(intersected.element);
d3// Select ALL children elements within our intersected element...
.selectChildren()
// Specify that any changes to the object will take 1000 milliseconds (1 sec)
.transition()
.duration(1000)
// Change style to be fully visible
.style("opacity", "1")
; }
Setup and initialise a scrollama object
We’ve now setup our workspace and created the function that will be triggered when we intersect with an element. Now we just need to setup our scrollama object and then initiliase it. The scrollama().setup()
function has two basic arguments we should specify (see full documentation here):
- step: Define which elements will be monitored by intersection observer. In our case “.level2” ensures that we will check for intersection with all elements that have class ‘.level2’ (i.e. every section starting with ## TITLE in Markdown).
- offset (0-1): How far from the top of the viewport (i.e. page) should we check for intersection? 0 = top of page, 1 = bottom of page. In this case, we’re triggering close to the bottom of the page so the reader doesn’t have to scroll into the blank space.
We then need to specify when the function we created above will be triggered. We have a few options, most relevant are .onStepEnter()
(triggered when we start intersecting an element) and .onStepExit()
(triggered when we stop intersecting an element). In our simple case we’ll just trigger on enter.
// Setup a scrollama object
scrollama()
.setup({
// Specify which elements will trigger our events
step: ".level2",
// Specify how far from top of page triggers occur
offset: 0.85
})// Specify when our function is triggered
.onStepEnter(handleStepEnter);
Bringing it together
So now we’ve completed all our steps. We’ve setup our workspace by loading our libraries and making all our sections invisible (opacity 0) to begin with. Next, we created a function handleStepEnter()
that specifies that elements should become visible (opacity 1) when we intersect with them. Finally, we setup our scrollama()
object to specify what type of elements will be triggered and when they will be triggered (entering or exiting). We can add all of this into a <script>
element and put this all at the end of our Quarto document. Now each of our level 2 headers (## TITLE) will fade in as we scroll down. Interactive scrolling complete!
<script src="https://unpkg.com/scrollama"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
// Make everything w/ low opacity to start
.selectAll(".level2")
d3.selectChildren()
.style("opacity", 0)
// Create scrollama event handlers
// intersected = { element, direction, index }
const handleStepEnter = (intersected) => {
let el = d3.select(intersected.element);
.selectChildren()
el.transition()
.duration(1000)
.style("opacity", "1")
;
}
scrollama()
.setup({
step: "#quarto-document-content .level2",
offset: 0.85
}).onStepEnter(handleStepEnter);
</script>