Recently for my personal website (type9.dev) I wanted to do a single scroll style website which is organized by sections. One of the strengths of a single scroll website is that it “flows”. It’s easy to have a reader be exposed to a large amount of content because, if they’re interested, they will simply keep scrolling.
The pitfall, however, is that structure is awkwardly restricting — such that when needing to present a large amount of information, it is hard to organize as you can only separate information vertically. As a result, long sections of information might become exhausting to read if not organized cleverly.
A great way to solve this issue is to use “special effects” to “cue” in the entrance of new information in a scrolling website. For example, simply changing the highlighted section in a nav to remind the reader what section they’re in — or it could be as complicated as changing the theme of the surrounding elements to signify a change in content. Unfortunately there are technical complications to implementation; so I wanted to show my favorite solution I found for React.
The above example shows that we can track any arbitrary number of scrolling elements by wrapping the sections in a Scroller component, which we will build.
Notes before I start
I’m writing these components in TypeScript, but typing can be omitted if you’re writing in vanilla JS.
For this implementation I’m going to be using n8tb1t’s use-scroll-position hook.
This implementation is SSR friendly.
To define the problem we have a list of section components which each need to be tracked to figure out which one is currently in view.
Line 3— We need a section state variable which tracks the index number of the section we’re currently on
Line 10 — We create a Scroller element. This is the main parent element which will give its children hooks to be tracked. We need to pass in setSection so the Scroller knows how to tell the rest of the page when to change. How to code this will be discussed next
Now that the page has been set up, we need to implement the Scroller component such that it embeds every section with with the data necessary to track its own position and update our state variable accordingly.
This component will act as a component that will “wrap” around the sections create a hook for each child to track its own position.
Line 18 — The most important part of this component is what arguments we are passing into our sectionPositionHook.
- boundingRef → The reference to the large element that we’re rendering our sections into, since we’re not tracking the entire page’s scroller, but the scroll inside this particular bounding element.
- sectionRef → The reference to smaller section element that’s being rendered. This is the actual thing that is being tracked in the context of the boundingRef.
- sectionIndex → The smaller section needs to know what index number it is. That way we can simply run setSection(sectionIndex) to change the state at the right time.
- setSection → The mutator method we use to change the section which controls the state from our Index component
Finally, we will write the magic. Here we’re going to write a hook that uses n8tb1t’s use-scroll-position hook to track the positions of each section. I recommend writing this inside scroller.tsx file since this component is dependent on the implementation of our Scroller component.
The above hook should change the current Section state variable when the top-left of the section passes the midpoint depending on if triggered going up or down.
In the future, I would like to build this with better compatibility with routing to change the url and make it specific content more shareable. But there might be a way to integrate with a library like React Router or something similar.
Keep in mind for each section a scroll event listener is being made so be careful not use too many. But for simple websites this should be effective and potentially even dynamic depending on your implementation. Consider throttling the hook with a higher ms timeout if resources are a concern.
I will likely finalize the code and publish in a React component to wrap everything up; but for now, just the inner-workings so it can be customized and implemented as needed.