A scroll animation with React hooks
October 11, 2021
On the marketing page of my web development agency I use a video loop of a running greyhound dog as a hero image. In this article I described how I used React hooks to animate this loop, so the dog runs across the screen when the user scrolls down. Check it out here.
The objectives
- When the user scolls down the element should move to the right.
- It should start moving slowly and accelerate.
- Its position should be reset when the user scrolls up.
- This one-off animation on the landing page should use as little code as possible, so instead of using a scroll hook from a library we’ll roll our own.
The event listener and useEffect
Let’s start off with the basics: adding an event listener for the scroll event and logging the scroll value to the console. We use a useEffect hook to add the event listener after the DOM has loaded. We want the effect to run only once when the component is mounted, so weadd an empty dependency array. The effect returns a function that removes the event listener to prevent a memory leak.
The window object and server-side rendering
To prevent a ‘window is not defined’ error while server-side rendering, the getScrollPosition function returns zero if window is undefined.
function getScrollPosition() {if (typeof window === "undefined") return 0;return window.scrollY;}useEffect(() => {function logScroll() {console.log(getScrollPosition());}window.addEventListener('scroll', logScroll);return () => {window.removeEventListener('scroll', logScroll);};}, []); // empty array means the effect will run only once
Using transform: translateX to move the element
Now let’s use the scroll position to move the element. First we need a ref to the element, so that we can manipulate it’s position. Then we change its position using the style.transform property.
const element = useRef(null);useEffect(() => {function moveElement() {element.current.style.transform = `translateX(${getScrollPosition()}px)`}window.addEventListener('scroll', moveElement);return () => {window.removeEventListener('scroll', moveElement);};}, [])return (<div ref={element} />)
requestAnimationFrame to the rescue
Next we’ll use the requestAnimationFrame function to tell the browser that we’re going to start shifting the furniture around. The browser will make sure that the animation functions gets called before the next repaint.
function handleScroll() {requestAnimationFrame(moveElement)}window.addEventListener('scroll', handleScroll);return () => {window.removeEventListener('scroll', handleScroll);};
A home grown ease-in function
The next step is to change the movement of the element so that it starts to move slowly and accelerate as the user is scrolling down. To achieve this we’ll use an exponential function. The divisor controls the initial slow down and the exponent changes the acceleration.
function easeIn({ value, divisor, exponent }) {return Math.pow(value / divisor, exponent)}
Capturing the scroll direction
The last step is to reset the position of the element when the user scrolls up. To capture the scroll direction we add a ref that holds the value of the scroll position at the last scroll event. By comparing this value to the current scroll position we determine whether the user is scrolling up. A second ref holds the value of the current scroll direction.
const lastPosition = useRef(getScrollPosition());const isScrollingUp = useRef(false);function setScrollDirection() {const scrollPosition = getScrollPosition();if (scrollPosition < lastPosition.current) {isScrollingUp.current = true;} else {isScrollingUp.current = false;}lastPosition.current = scrollPosition;}
const offset = isScrollingUp.current? 0: easeIn({value: getScrollPosition(),divisor: 50,exponent: 4,});
Putting it all together
To see how all the pieces work together here’s an interactive stack blitz preview and editor that shows how all the code is working together. Click on the editor tab to view and tweak the code.