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.
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>
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: [],
};
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>
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>
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>
<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>
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)