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.
Top comments (0)