DEV Community

Cover image for Lightweight React AoS
Hossam A.Menem
Hossam A.Menem

Posted on

Lightweight React AoS

In today's modern websites, having animation in your website is a must, and some simple animations can add a lot to your website, that's why AoS library is so popular, but what about creating an alternative for it on your own?

Our starting point

I didn't write the code for this component from scratch, the code I started from was by Bret Cameron, and from the next section in this blog, I'll assume that you went through his code and got an idea about what this component does.

Note
When I copied Bret's code from his codepen, I got an issue because of how he imported the types, I got this error 'X' refers to a value, but is being used as a type here. Did you mean 'typeof X'?, with X being any type I'm importing like CSSProperties or RefObject, I solved this error by using import { ... } from "react" instead of const {...} = React.

Issues with this code

overriding default values

there is no way to change the values for the defaultStyles in the AnimateIn function, which is quite problematic because you might want to add a delay for some sections, change the easing option...etc. To solve this issue let's first create an interface for our animation components.

interface IAnimation {
  children: ReactNode;
  transitionProps?: CSSProperties;
}
Enter fullscreen mode Exit fullscreen mode

Now let's use this interface in our animation components, and let's just try the FadeIn.

const FadeIn = (props: IAnimation) => (
  <Animate
    from={{ opacity: 0 }}
    to={{ opacity: 1 }}
    transitionProps={props.transitionProps}
  >
    {props.children}
  </Animate>
);
Enter fullscreen mode Exit fullscreen mode

Note: I changed the name of AnimateIn function to Animate.

Since we have now passed our transitionProps we should handle them in our Animate function, creating an interface for that is not necessary but I just like to use interfaces.

interface IAnimate extends IAnimation {
  from: CSSProperties;
  to: CSSProperties;
}
Enter fullscreen mode Exit fullscreen mode

Let's add a condition to handle when to use the default props and when to use the passed props like this:

const styleProps = props.transitionProps
    ? props.transitionProps
    : defaultStyles;
Enter fullscreen mode Exit fullscreen mode

And we just use styleProps as we did with the defaultStyles.

A "run once" option

Now, Everything runs perfectly, but there is one issue, that I'd consider a huge one, which is when you scroll down and one of the sections that you are observing is now on the screen and its animation has started then you scroll down and go back up the animation will start again, in most websites you don’t see that, because it’s kinda annoying to have animations that start every time you scroll to a section, so for that reason, we should add a "run-once" option, but I'm not gonna make it the only option, because there might be some use cases where the animation have to start every time the user scrolls to the section. First of all, we update our IAnimation interface to have a runOnce prop.

interface IAnimation {
  children: ReactNode;
  runOnce?: boolean;
  transitionProps?: CSSProperties;
}
Enter fullscreen mode Exit fullscreen mode

Also, add a parameter to the useElementOnScreen hook to handle the run once mode.

const useElementOnScreen = (
  ref: RefObject<Element>,
  runOnce: boolean | undefined
) => { ... }
Enter fullscreen mode Exit fullscreen mode

And the logic for it is fairly simple, everything runs as normal, we keep observing the section that we want, and we check if it has been on the screen, if it does ( the animation has started ), then we check if runOnce is true, if it is we unobserve this section, otherwise, we just keep observing and triggering the animation again and again when the element enters the screen. So let's take the process for this one step by step.

  1. First we specify the runOnce argument in one of our animation divs like this.

    <AoS.FadeUp runOnce>
     <AnySection/>
    </AoS.FadeUp>
    

    Note: the default for this option is true, so you don't have
    to specify it unless you want to set it to false.

  2. since we have its value, we can now access it via the useRef hook in the Animate, then pass it to the useElementOnScreen hook like this.

    const { current: runOnce } = useRef(props.runOnce);
    const ref = useRef<HTMLDivElement>(null);
    const onScreen = useElementOnScreen(ref, runOnce);
    
  3. Inside the useElementOnScreen hook we will add an if statement for that option like this.

const useElementOnScreen = (
  ref: RefObject<Element>,
  runOnce: boolean | undefined
) => {
  const [isIntersecting, setIsIntersecting] = useState(true);
  useEffect(() => {
    const observer = new IntersectionObserver(
    ([entry]) => {
      if (runOnce && ref.current && entry.isIntersecting) {
        observer.unobserve(ref.current);
     }
  ...
Enter fullscreen mode Exit fullscreen mode

In this if statement, first we check if the runOnce option is true, then we check if the element that we are passing exists, and last with entry.isIntersecting we check if the element has transitioned into the intersection state, which means that It has been on the screen and its animation has started.

For more information about the intersection observer API visit the MDN docs.

And that's it...

Now you can just copy this file, add any your changes and start using it in your next project.

If you wanna see some of those animations on a live website, I'm already using them in my portfolio, so feel free to check it out.

Top comments (0)