DEV Community

Adam Karmiński
Adam Karmiński

Posted on • Edited on

Use IntersectionObserver with Astro and Tailwind to create "reveal on scroll" animations

When working on dailyboard.io website, I was searching the web for an easy-to-use tutorial on how to use IntersectionObserver with Astro. No luck. I didn't want to add a library to my bundle, so I just decided to figure it out by myself.

First, I wanted to put the code in the setup section of my Astro component. However, by seeing IntersectionObserver is not defined I quickly realised this code is executed on the server, where there's no IntersectionObserver.

Image description

That's when I learned, you can actually use a regular <script> tag in an Astro component to run some client-side code.

So in my Layout.astro I wrote a simple script at the end of <body>:

<body>
    <slot />
    <Footer />
    <script>
        const observer = new IntersectionObserver(
            (elements) => {
                elements.forEach((el) => {
                    const animation = el.target.getAttribute('data-animate');

                    if (animation && el.isIntersecting) {
                        el.target.classList.add(animation);
                        el.target.classList.remove('opacity-0');
                    }
                });
            }
        );
        const elements = document.querySelectorAll('[data-animate]');
        elements.forEach((el) => {
            el.classList.add('opacity-0');
            observer.observe(el);
        });
    </script>
</body>
Enter fullscreen mode Exit fullscreen mode

Then, I created some simple, custom animations in tailwind.config.js:

module.exports = {
    content: ['./src/**/*.{astro,ts,vue}'],
    theme: {
        extend: {
            animation: {
                'fade-left': 'fadeLeft 800ms ease-in-out',
                'fade-right': 'fadeRight 800ms ease-in-out',
            },
            keyframes: {
                fadeLeft: {
                    '0%': {
                        transform: 'translate(-20%, 0)',
                        opacity: 0,
                    },
                    '100%': {
                        transform: 'translate(0,0)',
                        opacity: 100,
                    },
                },
                fadeRight: {
                    '0%': {
                        transform: 'translate(20%, 0)',
                        opacity: 0,
                    },
                    '100%': {
                        transform: 'translate(0,0)',
                        opacity: 100,
                    },
                },
            },
        },
    },
    variants: {
        extend: {},
    },
    plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

Now I was ready to stitch everything together! I just put data-animate="animate-fade-left" or data-animate="animate-fade-right" to any element I wanted to animate once it appeared in the viewport!

<div class="..." data-animate="animate-fade-left">...</div>
<div class="..." data-animate="animate-fade-right">...</div>
Enter fullscreen mode Exit fullscreen mode

It was a good start, but I decided to tweak the animations a bit. I decided to set a threshold for the IntersectionObserver, so that it fired once at least 35% of an element was visible in the viewport.

<body>
    <slot />
    <Footer />
    <script>
        const observer = new IntersectionObserver(
            (elements) => {
                elements.forEach((el) => {
                    const animation = el.target.getAttribute('data-animate');

                    if (animation && el.isIntersecting) {
                        el.target.classList.add(animation);
                        el.target.classList.remove('opacity-0');
                    }
                });
-           }
+           },
+           { threshold: 0.35 }
        );
        const elements = document.querySelectorAll('[data-animate]');
        elements.forEach((el) => {
            el.classList.add('opacity-0');
            observer.observe(el);
        });
    </script>
</body>
Enter fullscreen mode Exit fullscreen mode

The last thing I wanted to do, is to delay some animations to manage my viewers' focus and attention. I used setTimeout and a data-delay HTML attribute to control the animation right from the animation setup.

<div class="..." data-animate="animate-fade-left">...</div>
<div class="..." data-animate="animate-fade-right" data-delay="300">...</div>
Enter fullscreen mode Exit fullscreen mode
<body>
    <slot />
    <Footer />
    <script>
        const observer = new IntersectionObserver(
            (elements) => {
                elements.forEach((el) => {
                    const animation = el.target.getAttribute('data-animate');
+                   const delayData = el.target.getAttribute('data-delay');
+                   const delay = delayData ? Number.parseInt(delayData) : 0;

                    if (animation && el.isIntersecting) {
+                       setTimeout(() => {
                            el.target.classList.add(animation);
                            el.target.classList.remove('opacity-0');
+                       }, delay);                      
                    }
                });
        },
        { threshold: 0.35 }
        );
        const elements = document.querySelectorAll('[data-animate]');
        elements.forEach((el) => {
            el.classList.add('opacity-0');
            observer.observe(el);
        });
    </script>
</body>
Enter fullscreen mode Exit fullscreen mode

And that's it! With a few lines of code, I was able to achieve the desired result and ended up with a pretty versitile setup for controlling the animations.

Top comments (0)