DEV Community

Cover image for Animating keyline on scroll with React and TypeScript
Joana Moreira
Joana Moreira

Posted on • Originally published at joanamoreira.hashnode.dev

Animating keyline on scroll with React and TypeScript

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/>
Enter fullscreen mode Exit fullscreen mode

We then add some basic styling...

.keyline {
        height: 5px;
        background-color: coral;
}
Enter fullscreen mode Exit fullscreen mode

...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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 })}  />
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
/* 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;
  }
}
Enter fullscreen mode Exit fullscreen mode
// 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;
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)