Animating keyline on scroll with React and TypeScript

Photo by KOBU Agency on Unsplash

Animating keyline on scroll with React and TypeScript

Easy pure CSS and React animation

Featured on Hashnode

Adding subtle CSS animations to a website helps bring it to life but sometimes we only want the user to see them as they scroll through the page.

This is a very simple tutorial for incorporating that behavior into a React and TypeScript application using a custom hook.

Let’s dive in!

Creating our keyline element

First, we’ll create a div with two classes, keyline and animate.

<div className=”keyline animate”/>

We then add some basic styling...

.keyline {
        height: 5px;
        background-color: coral;
}

...and we add CSS animations to the keyline element.

.animate {
  animation: swing-in-left-bck 3s cubic-bezier(0.175, 0.885, 0.32, 1.275) both;
}

@keyframes swing-in-left-bck {
  0% {
    transform: rotateY(-70deg);
    transform-origin: left;
    opacity: 0;
  }
  100% {
    transform: rotateY(0);
    transform-origin: left;
    opacity: 1;
  }
}

Animations

You can find the animation we've used and more at https://animista.net/play/entrances/swing-in/swing-in-left-bck.

The animation works as expected but it gets triggered on the initial render which we don’t want - so let’s fix that!

Writing our custom hook

We want to write a custom hook that will allow us to detect whether the element (our div) is visible on the screen.

We’ll be using the IntersectionObserver API which is perfect for lazy loading images or triggering animations when the user has scrolled down to a particular section.

import { useEffect, useState, MutableRefObject } from "react";

const useOnScreen = <T extends Element>(
  ref: MutableRefObject<T>,
  rootMargin: string = "0px"
): boolean => {
  const [isIntersecting, setIntersecting] = useState<boolean>(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        setIntersecting(entry.isIntersecting);
      },
      {
        rootMargin
      }
    );

    observer.observe(ref.current);

    return () => {
      observer.disconnect();
    };
  }, []);
  return isIntersecting;
};

export default useOnScreen;

This hook allows you to easily detect when an element is visible as well as specify how much of the element should be visible before being considered on screen.

It will return a boolean (isIntersecting) that we can use to trigger our animations.

Updating the animations

Back to our keyline div, let’s make the necessary changes to make use of our new hook!

<div className={clsx("keyline", { animate: onScreen })}  />

We’re using clsx to conditionally pass our animate class based on a boolean called onScreen.

Using the useOnScreen hook

Let’s see how we can implement onScreen using the useOnScreen hook.

const ref = useRef<HTMLDivElement>();

const onScreen: boolean = useOnScreen<HTMLDivElement>(ref, "-100px");

As you can see, we created a ref that will be attached to the element to be used by the IntersectionObserver. We've also set the root margin to be 100px (from the bottom of the screen).

Pretty easy huh? Here’s the full code.

// App.tsx
import { useRef } from "react";
import clsx from "clsx";
import "./styles.css";
import useOnScreen from "./useOnScreen";

export default function App() {
  const ref = useRef<HTMLDivElement>();

  const onScreen: boolean = useOnScreen<HTMLDivElement>(ref, "-100px");

  return (
    <div className="App">
      <h1>Scroll down</h1>
      <div className="box" />
      <div className={clsx("keyline", { animate: onScreen })} ref={ref} />
    </div>
  );
}
/* styles.css */
.box {
  background-color: #cccccc;
  height: 100vh;
}

.keyline {
  height: 5px;
  background-color: coral;
}

.animate {
  animation: swing-in-left-bck 3s cubic-bezier(0.175, 0.885, 0.32, 1.275) both;
}

@keyframes swing-in-left-bck {
  0% {
    transform: rotateY(-70deg);
    transform-origin: left;
    opacity: 0;
  }
  100% {
    transform: rotateY(0);
    transform-origin: left;
    opacity: 1;
  }
}
// useOnScreen.ts
import { useEffect, useState, MutableRefObject } from "react";

const useOnScreen = <T extends Element>(
  ref: MutableRefObject<T>,
  rootMargin: string = "0px"
): boolean => {
  const [isIntersecting, setIntersecting] = useState<boolean>(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        setIntersecting(entry.isIntersecting);
      },
      {
        rootMargin
      }
    );

    observer.observe(ref.current);

    return () => {
      observer.disconnect();
    };
  }, []);
  return isIntersecting;
};

export default useOnScreen;

And that’s it! Hope you found this useful and make sure to check out the full example.