How to create slide-in-on-scroll animations in a React app
I was hunting for quite a while today trying to understand what simple website-builder themes do with seemly every website. I wanted to add a little dynamic movement to my webpage, and had originally thought that CSS transitions were enough to do this. Normally, they are.. but not when we’re scrolling.
Today, we’ll create a React hook that makes use of the IntersectionObserver API in Javascript to create a div that fades in from the left (like the gif below). Once you’ve created this hook, it’ll be easy to modify the effect to suit your needs.
Set up
I won’t get into how to create a React project from scratch. These examples use TailwindCSS and a sprinkle of Typescript, but these shouldn’t get in the way if you’re unfamiliar with them.
Let’s say we have web app with the following home page:
import AnimateOnScroll from '@/components/common/AnimateOnScroll'
import React from 'react'
interface SampleComponentProps {
children: React.ReactNode
}
const SampleComponent = ({ children }: SampleComponentProps) => {
return (
<div className='h-screen grid grid-cols-1 place-items-center'>
<h1 className='bg-blue-700 p-20 rounded-xl text-white '>{children}</h1>
</div>
)
}
const HomePage = () => {
return (
<>
<SampleComponent>I'm a normal div</SampleComponent>
<AnimateOnScroll reappear>
<SampleComponent>I appear on Scroll!</SampleComponent>
</AnimateOnScroll>
</>
)
}
export default HomePage
The SampleComponent
is just a simple div with a blue background, but the idea is that this could be literally any page element. We’ll create the AnimateOnScroll
component next, the idea being that we need simply wrap any div inside to create this animation effect when the user scrolls.
The Javascript Intersection Observer API
This native API in Javascript provides a way for a referenced element’s position to be observed by its ancestors, or the viewport. This API hasn’t been around forever, so older implementations of this once required a lot more javascript to make it happen.
By the way, if you need to do more advanced animations and transitions, two very popular libraries for making such effects are framer-motion and react-spring.
You would instigate the Intersection observer in this way:
let observer = new IntersectionObserver(callback, options);
and then have the functions observer.observe()
and observer.unobserve()
interact with the referenced element.
Let’s create a new React hook
Here’s a React hook that will take a set of options and pass back a reference to an element, plus a flag indicating whether the element is in the viewport at present.
import { useEffect, useRef, useState } from "react";
type Options = {
threshold: number,
reappear?: boolean,
}
const useElementOnScreen = (options: Options): [React.RefObject<HTMLDivElement>, boolean] => {
const containerRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState<boolean>(false);
const makeAppear = (entries: any) => {
const [entry] = entries;
if (entry.isIntersecting)
setIsVisible(true);
};
const makeAppearRepeating = (entries: any) => {
const [entry] = entries;
setIsVisible(entry.isIntersecting);
};
const callBack = options.reappear ? makeAppearRepeating : makeAppear;
useEffect(() => {
const containerRefCurrent = containerRef.current
const observer = new IntersectionObserver(callBack, options);
if (containerRefCurrent)
observer.observe(containerRefCurrent);
return () => {
if (containerRefCurrent) {
observer.unobserve(containerRefCurrent);
}
};
}, [containerRef, options, callBack]);
return [containerRef, isVisible];
};
Here we use theuseRef
hook along with the intersection observer, so we can infer whether the element is in the viewport. There’s a threshold value here that the hook needs; this value determines how far the user has scrolled before the element appears. We dont want the animation happening the second the element appears at the top – halfway down is a little better — therefore 0.5 is usually a good value for this threshold.
I’ve also added a parameter called reappear.
The idea with this is to give the user of the hook the option to make the element reset itself and run the transition again every time the user scrolls up and down past the element, or alternatively, for it just to happen once.
Using the new hook
If you’re looking to just copy paste a full component to use, here you go:
import { useEffect, useRef, useState } from "react";
type Props = {
children: React.ReactNode;
reappear?: boolean;
threshold?: number;
};
type Options = {
threshold: number,
reappear?: boolean,
}
const useElementOnScreen = (options: Options): [React.RefObject<HTMLDivElement>, boolean] => {
const containerRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState<boolean>(false);
const makeAppear = (entries: any) => {
const [entry] = entries;
if (entry.isIntersecting)
setIsVisible(true);
};
const makeAppearRepeating = (entries: any) => {
const [entry] = entries;
setIsVisible(entry.isIntersecting);
};
const callBack = options.reappear ? makeAppearRepeating : makeAppear;
useEffect(() => {
const containerRefCurrent = containerRef.current
const observer = new IntersectionObserver(callBack, options);
if (containerRefCurrent)
observer.observe(containerRefCurrent);
return () => {
if (containerRefCurrent) {
observer.unobserve(containerRefCurrent);
}
};
}, [containerRef, options, callBack]);
return [containerRef, isVisible];
};
const AnimateOnScroll = ({ children, reappear, threshold = 0.5 }: Props) => {
const [containerRef, isVisible] = useElementOnScreen({
threshold: threshold,
reappear: reappear,
});
return (
<>
<div ref={containerRef} className={`transition duration-1000 ${isVisible ? "opacity-100 blur-none translate-x-0" : "opacity-0 blur-lg -translate-x-20"} motion-reduce:transition-none motion-reduce:hover:transform-none`}>
{children}
</div>
</>
);
}
export default AnimateOnScroll;
Place the above in a file and name it something like AnimateOnScroll.tsx.
You may then wrap any element with this component, and the scroll effects should just work. If you’re not super familiar with Tailwind, let me quickly explain the CSS:
className=`transition duration-1000
${isVisible ? "opacity-100 blur-none translate-x-0" : "opacity-0 blur-lg -translate-x-20"}
motion-reduce:transition-none motion-reduce:hover:transform-none`
Our hook has given us a state variable,isVisible
, that tells us whether the element is in the viewport. If it isn’t, the inline ternary operator will blur the element and shift it to the left of where it should be, and make it totally transparent.
Then, transition
will allow a smooth unblurring, reset the position, and show the element as soon as it comes into view.
It’s also a good idea to set the prefers-reduced-motion settings for users that want to limit effects like this. Always remember the importance of accessibility when creating such effects.
Final thoughts
If you copy the first code snippet and then the last, you should have a working version of this transition-on-scroll effect. Now you can play around with the transitions (using tailwind or just vanilla CSS) to get them the way you want. You can adjust the blur, initial position, opacity, and any other effects you like, now that the transition can trigger on scroll.
This article was inspired by this awesome video by Fireship, extended for React.