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.

© 2024 Bret Cameron