Animating keyline on scroll with React and TypeScript
Tags: react, typescript, css, javascript
Published: 2022-11-26 14:36:19

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.

{% codesandbox animated-keyline-r6f4hs %}

Let’s dive in!

## Creating our keyline element

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

```ts
<div className=”keyline animate”/>
```

We then add some basic styling...

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

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

```css
.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](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](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) which is perfect for lazy loading images or triggering animations when the user has scrolled down to a particular section.

```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;
```

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!

```js
<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.

```ts
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.


```ts
// 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>
  );
}
```


```css
/* 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;
  }
}
```


```ts
// 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](https://codesandbox.io/s/animated-keyline-r6f4hs).