Simple React Scroll Animations With Zero Dependencies
React Hooks, CSS, and the Intersection Observer API
Published on
Sep 13, 2022
Read time
3 min read
Introduction
One of the most common effects in brochure websites is a fade-in or float-in animation that occurs during scrolling, shortly after the element enters the user’s screen.
To achieve these effects, developers typically reach for an animation library. But for most websites, there is no need for this additional overhead!
Using CSS and the native Intersection Observer API, we can create a simple and performant method for animating elements as users scroll down our page. In this article, we’ll see how to achieve this with React. The examples in this article also use TypeScript.
Step 1: Detect When an Element Is on Screen
Whenever one of our React components is mounted, we can create a new IntersectionObserver
that will observe when it intersects with the browser viewport.
The preferred way to reference elements in React is via refs, so we’ll use ref as our first argument. The second argument allows us to set an offset amount so that the animation triggers a set distance from the edge of the window — with "0px"
being exactly on the edge.
function useElementOnScreen(ref: RefObject<Element>, rootMargin = "0px") {
const [isIntersecting, setIsIntersecting] = useState(true);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setIsIntersecting(entry.isIntersecting);
},
{ rootMargin }
);
if (ref.current) {
observer.observe(ref.current);
}
return () => {
if (ref.current) {
observer.unobserve(ref.current);
}
};
}, []);
return isIntersecting;
}
Make sure to run unobserve
in the returned cleanup function so we don’t continue observing a component once it has dismounted.
Step 2: Create a Container Component
For a simple fade-in-and-up effect, we can simply alter the opacity
and translate
CSS properties depend on whether the element is on screen or not.
const AnimateIn: FC<PropsWithChildren> = ({ children }) => {
const ref = useRef<HTMLDivElement>(null);
const onScreen = useElementOnScreen(ref);
return (
<div
ref={ref}
style={{
opacity: onScreen ? 1 : 0,
translate: onScreen ? "none" : "0 2rem",
transition: "600ms ease-in-out",
}}
>
{children}
</div>
);
};
Then, in our code, we can simply call:
<AnimateIn>
<h1>Hello World</h1>
</AnimateIn>
We could stop here, but with a few additional tweaks, we can turn this pattern into something more customisable and reusable.
Step 3: Creating a Library of Animations
If we want to extend the range of possible animations, we need to make our component a little more generic, allowing developers to pass in their own CSS for the from
and to
states.
const AnimateIn: FC<
PropsWithChildren<{ from: CSSProperties; to: CSSProperties }>
> = ({ from, to, children }) => {
const ref = useRef<HTMLDivElement>(null);
const onScreen = useElementOnScreen(ref);
const defaultStyles: CSSProperties = {
transition: "600ms ease-in-out",
};
return (
<div
ref={ref}
style={
onScreen
? {
...defaultStyles,
...to,
}
: {
...defaultStyles,
...from,
}
}
>
{children}
</div>
);
};
Now, if we wanted to create a library of simple animations, we could easily build this using our AnimateIn
component. For example:
const FadeIn: FC<PropsWithChildren> = ({ children }) => (
<AnimateIn from={{ opacity: 0 }} to={{ opacity: 1 }}>
{children}
</AnimateIn>
);
const FadeUp: FC<PropsWithChildren> = ({ children }) => (
<AnimateIn
from={{ opacity: 0, translate: "0 2rem" }}
to={{ opacity: 1, translate: "none" }}
>
{children}
</AnimateIn>
);
const ScaleIn: FC<PropsWithChildren> = ({ children }) => (
<AnimateIn from={{ scale: "0" }} to={{ scale: "1" }}>
{children}
</AnimateIn>
);
As a personal preference, I like grouping these components as part of an Animate
object, which I then export:
export const Animate = {
FadeIn,
FadeUp,
ScaleIn,
};
We can then call an animation like this:
<Animate.ScaleIn>
<h1>Hello World</h1>
</Animate.ScaleIn>
We now have access to a comparable level of animation to most brochure websites without needing additional dependencies!
For a working example, check out this CodePen.
Related articles
You might also enjoy...
Bad Abstractions Could Be Ruining Your Code
Why the ‘Don’t Repeat Yourself’ principle might be doing more harm than good
5 min read
How to learn to code and land your first job in 2024
How I would get my first job as a self-taught developer if I started over
7 min read
Building an International Website With Next.js
3 solutions to toggling routes on and off for different regions
7 min read