Performing scroll-triggered animations using Intersection Observer API with React
In this guide, we will walk through how to create powerful scroll-triggered animations using the browser Intersection Observer API by building a simple website with a sticky back-to-top button. The Intersection Observer API provides an asynchronous way of detecting changes in the visibility of a DOM Element within a parent element or the document viewport.
Before the implementation of the Intersection Observer API, developers detect whether an element is visible in the DOM by calling the getBoundingClientRect method of the element. This method runs synchronously on the main thread and can cause serious performance issues.
The Intersection Observer API is used internally by animation libraries like Framer Motion for scroll animations.
Prerequisites
We recommend the following prerequisites before getting started.
Familiarity with Javascript and React library
Node.js installed on your machine
Getting Started
In this section, we will bootstrap a simple React application using Vite and install other dependencies.
npx create vite@latest intersection-observer -- --template react
cd intersection-observer
npm install
Install the Tailwind CSS library and its dependencies and configure them to help with styling.
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Add the path to your React component files where tailwind CSS classes will be used in the tailwind.config.js.
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
Delete all existing styles and add the tailwind directive to your index.css file.
@tailwind base;
@tailwind components;
@tailwind utilities;
Start the Vite development server and visit the URL logged on your terminal, which defaults to localhost:5173.
npm run dev
Building the React Application
Next up, we will be breaking the application down by creating three simple React components and a custom hook. We will be creating a Header
, BackToTop
and ChevronUpIcon
React components. The BackToTop
component becomes visible when some part of the Header
component disappears as the page is scrolled down and when clicked, scrolls up to the Header
component. We will also create a custom React hook that utilizes the Intersection Observer API to check for the presence of an element in the viewport.
mkdir src/components
touch src/components/Header.jsx
touch src/components/BackToTop.jsx
touch src/components/ChevronUpIcon.jsx
mkdir src/hooks/usePresence.js -p
Creating the Header component
Inside our src/components/Header.jsx
file:
import { forwardRef } from "react";
const Header = forwardRef((props, ref) => {
return (
<header
ref={ref}
className="p-3 flex items-center bg-slate-600"
{...props}
>
<h2 className="text-md font-semibold text-white">Observer</h2>
</header>
);
});
export default Header;
Creating the ChevronUpIcon component
Inside our src/components/ChevronUpIcon.jsx
file:
const ChevronUpIcon = (props) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 15.75l7.5-7.5 7.5 7.5"
/>
</svg>
);
};
export default ChevronUpIcon;
Creating the BackToTop component
Inside your src/components/BackToTop.jsx
file:
import ChevronUpIcon from "./ChevronUpIcon";
const BackToTop = ({ isVisible, onClick }) => {
if(!isVisible) {
return <div />
}
return (
<div
role='button'
className="fixed w-9 h-9 lg:w-10 lg:h-10 p-1 bottom-5 right-4 text-white bg-slate-500 rounded-full z-40 shadow-sm flex items-center justify-center"
onClick={onClick}
>
<ChevronUpIcon className="w-6 h-6 md:h-7 md:w-7" />
</div>
);
};
export default BackToTop;
In the BackToTop
component, if the isVisible
prop is true, we render the main content else we render an empty div
. We also passed an onClick
prop to the onClick
attribute.
Creating the usePresence custom hook
Inside your src/hooks/usePresence.js
file:
import {useEffect, useState} from 'react';
const usePresence = (ref, options = {}) => {
const [isEntering, setIsEntering] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
setIsEntering(entries[0].isIntersecting);
}, options)
if(ref.current) {
observer.observe(ref.current);
}
return () => {
observer.disconnect();
}
}, [ref])
return isEntering;
}
export default usePresence;
There is quite a bit happening in the code above, so let's walk through it:
We set up a state (
isEntering
) for tracking the current visibility of the element ref which is passed as a parameter to theusePresence
hook.Inside
useEffect
, an instance ofIntersectionObserver
is created with a callback function that is always called asynchronously when the position of the element changes andoptions
.Inside this callback, the
isEntering
state is updated using theentries
argument.We start observing for changes in the position of the element by calling the
observe
method of theIntersectionObserver
instance.Once the component using this hook is unmounted, we stop observing for changes in the position of this element by calling the
disconnect
method of theIntersectionObserver
instance.The state
isEntering
is returned to the caller of this hook.
Updating our App component
Inside your src/App.jsx
file, update the existing code:
import { useRef } from "react";
import Header from "./components/Header";
import BackToTop from "./components/BackToTop";
import usePresence from "./hooks/usePresence";
function App() {
const ref = useRef(null);
const headerIsVisible = usePresence(ref, {threshold: 0.5});
const handleClick = () => {
if(ref.current) {
ref.current.scrollIntoView({
behavior: "smooth",
block: "center"
});
};
}
return (
<div>
<Header ref={ref} />
<BackToTop isVisible={!headerIsVisible} onClick={handleClick} />
<main className="min-h-[102vh]" />
</div>
)
}
export default App
Again, Let's break down the code above:
We create a React Ref and pass it to the
Header
component to get access to the root DOM node.We pass this Ref to the
usePresence
hook and start tracking for changes in the visibility of theheader
element, we also passed in anIntersectionObserver
option calledthreshold
which is set to 0.5. This option means that theIntersectionObserver
callback will be executed when the visibility of theheader
is 50%. Thethreshold
option also accepts an array of numbers.We create a
handleClick
handler and passes it to theBackToTop
component which is called when clicked to scroll back to theheader
component.The
headerIsVisible
variable returned by theusePresence
will be true if more than 50% of theheader
element is visible and false otherwise. We display theBackToTop
component when theheaderIsVisible
variable is false.
We now have a functional scroll-triggered animation for this simple website using Intersection Observer API. You can clone this github repo https://github.com/Chimise/intersection-observer if your app is not working as expected.