An Overlapping "Parallax" Effect on Next.js with Preserved Server-Side Rendering
Yevhen Petrunkin
Frontend Developer: Next.js | React | TypeScript | JavaScript etc.
Let's make it clear right from the start - this is NOT parallax, though sometimes customers might call it that way...
Recently, I took part in a project where the customers kept insisting they needed whole sections of their website to move with parallax on scroll, when finally it appeared they had meant a completely different thing.
Genuine parallax implies the background of a container and its content move with different speed, which creates an eye-catching optical illusion. When a few containers in a row are given this effect, they seem to start overlapping each other on scroll while they actually do NOT - they still go one after another and it is the speed discrepancy between the background and content inside each container that makes us believe the overlapping takes place. Here are some legit examples of parallax.
There are a few ways and lots of tutorials on how to achieve this effect. However, despite attractive looks, parallax can sometimes be rather inconvenient and even inapplicable.
For instance, when your website has not just one or two but several sections (containers) each having lots of important text content moving with parallax - at a certain point the user might not be able to see a great part of the content, because it will go away faster than it appears in front of the user's eyes.
Or, when your sections do not actually have any background image at all but rather a certain background color - creating attractive parallax can be quite challenging.
So, in these cases, we might want to apply something else, something as presentable but more practical - something like the effect shown in the following video.
Here, the sections ACTUALLY overlap each other, fold in "a pile" on scrolling down and unfold one by one on scrolling backwards.
There is a range of examples showing how we can make this effect with pure CSS (position: sticky) or with both CSS and some amount of JavaScript, including my own example - not very elegant though.
However, in some circumstances these options may cease to be helpful as well. For example, working on the project I started this article with, I ran into two problems:
Problem 1. All of the aforementioned options imply that each section's height should not exceed the viewport height. Otherwise, if a section which does not fit into the viewport stops moving upon reaching the top of the viewport - the user will not see a part of the content at the bottom of the section, and this is bad.
A possible way to solve this problem is by watching the offsetHeight of each section and comparing it with the innerHeight of the viewport:
Problem 2. In terms of React the above solution will require that we wrap all our sections in a wrapper component with a controlled state, where the offsetHeight of the current section will be watched and each time a decision will be made on when the current section must stop moving.
When it comes to Next.js, however, another matter of concern will inevitably arise here - obviously, our controlled-state wrapper must be a client-side component, but it would be quite nice if we preserved the sections with all their content as server-side components, otherwise the whole thing with Next.js would turn out to be of no good use to us, wouldn't it?
Luckily, Next.js allows us to preserve server-side rendering of the sections, if we insert them inside the client-side wrapper as its children, i.e. via children prop of the wrapper component.
<Wrapper> // client-side
<Section /> // server-side
<Section /> // server-side
<Section /> // server-side
</ Wrapper>
In my search for the best solution to the whole problem, which would support such configuration and fit into Next.js logic, after trying various libraries and custom options, I stopped at gsap.
Here you can see an example from the creators of gsap showing how we can make the overlapping effect using this library.
One good reason to pick this option is that unlike the rest of the examples mentioned previously it takes into account the fact that the sections may actually be of various height, and some of them may even be higher than the viewport!
Another advantage is that not all libraries (like react-spring or react-scroll-parallax) support this configuration - server-side components inside a client-side one - but gsap does.
On the other hand, there is still no explanation how to apply it to a React app not to say of Next.js, whereas a bunch of pitfalls related to such application does exist.
Therefore, I came up with my own more convenient solution based on gsap and now I am going to share it with you step by step.
1. First of all, in our Next.js application we need to install gsap library:
领英推荐
npm i gsap
2. Then, we are going to create a custom hook - like this:
import { useEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
export const useOverlapping = (ref) => {
useEffect(() => {
const ctx = gsap.context(() => {
const sections = gsap.utils.toArray(".section");
const headerRef = document.querySelector("header");
if (sections && headerRef) {
sections.forEach((section) =>
gsap.to(section, {
scrollTrigger: {
trigger: section,
start: () =>
section.offsetHeight <=
window.innerHeight - headerRef.offsetHeight
? `top ${headerRef.offsetHeight}px`
: "bottom bottom",
endTrigger: ref.current,
end: "bottom top",
scrub: true,
pin: section,
pinSpacing: false,
},
})
);
}
}, ref);
return () => ctx.revert();
}, [ref]);
};
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
export const useOverlapping = (ref) => {
useEffect(() => {}, [ref])
}
useEffect(() => {
const ctx = gsap.context(() => {}, ref);
return () => ctx.revert();
}, [ref])
Otherwise we would encounter strange and unpleasant behavior like sections "jumping" from top to bottom and changing their order or appearing of undesired spaces between sections etc.
Running gsap animation on Next.js without gsap context:
const sections = gsap.utils.toArray(".section");
const headerRef = document.querySelector("header");
sections.forEach((section) =>
gsap.to(section, {
scrollTrigger: {},
})
);
trigger: section,
In our case, in the callback we state the following: if the current section offsetHeight is smaller or equal to viewport innerHeight minus header offsetHeight (as we also have a header) - then the animation will start when the top of the section reaches the point where it meets the bottom of the header. Otherwise, it will happen when the bottom of the section reaches the bottom of the viewport.
start: () =>
section.offsetHeight <=
window.innerHeight - headerRef.offsetHeight
? `top ${headerRef.offsetHeight}px`
: "bottom bottom",
endTrigger: ref.current,
end: "bottom top",
In other words it will never happen, because when the last section becomes fixed ("pinned") the bottom of the wrapper element will stop moving and it will never reach the top of the viewport.
pin: section,
pinSpacing: false,
scrub: true,
markers: true,
3. We are almost done! Now we just need to create a special component in our Next.js application where the useOverlapping hook will apply and which will wrap all sections as its children. In my case, I called it OverlapClient. With the useRef hook from React library we get the reference to the wrapper element (<div>) and pass this reference to the useOverlapping hook as an argument. Just don’t forget to give each section a "section" className and specify "use server" directive inside the sections to preserve server-side rendering, because OverlapClient is a client-side component.
"use client";
import { useRef } from "react";
import { useOverlapping } from "@/hooks/useOverlapping";
const OverlapClient = ({ children }) => {
const wrapperRef = useRef();
useOverlapping(wrapperRef);
return (
<div ref={wrapperRef} className="pb-16">
{children}
</div>
);
};
export default OverlapClient;
Aftermath
Unfortunately, on the project I started the whole story with, in the end we had to refuse from this solution, since despite good looks and smooth performance it had one serious drawback that you need to take into account.
When using the website navigation menu, we come across one problem. The overlapping effect allows fast scrolling down to the needed sections successfully, but you cannot use the navigation menu to scroll backwards as all sections are already there folded "in a pile". So you can only scroll up manually. And you should keep this in mind when planning your website functionality, if you want to apply this overlapping "parallax" effect.
And this is it! If you find this article useful or interesting - please leave a reaction. And if you have some important remarks, additions, objections or ideas and suggestions on how to improve the code - feel free to share them in the comments.