javascriptvue.jsvuejs3nuxt.jsnuxt3.js

Intersection observer not triggering on page load and route back


I use useIntersectionObserver from VueUse to trigger a fade-in animation when an element enters the viewport. If I navigate to another page (e.g., click on an item in the carousel to view its details) and then go back to previous route with the saved position, the intersection observer doesn't trigger automatically, and the elements remain hidden unless I scroll the page down and up or refresh it. Also, if I scroll to the carousel, click on the carousel item and navigate to another route, the observer fails to trigger sometimes as well. So it's not only an issue when returning to a route, but also after scrolling and navigating to another route.

To hide the elements before the observer activates, I use the invisible class, which includes visibility: hidden and opacity: 0. The issue seems to be that intersection observer doesn't detect the elements when visibility: hidden is applied, so the fade-in animation never starts when returning to the page.

Demonstration: https://www.veed.io/view/e380440b-c04a-4508-a397-6448ac08a5d9?panel=share. Here I navigate to the /details route, then go back and then reload the page.

Observer.vue:

<template>
    <div ref="observerRef">
        <slot :isVisible="isVisible" />
    </div>
</template>

<script setup>
import { useIntersectionObserver } from '@vueuse/core';

const props = defineProps({
    rootMargin: {
        type: String,
        default: '0px',
    },
});

const observerRef = ref(null);
const isVisible = ref(false);

const { stop } = useIntersectionObserver(
    observerRef,
    ([{ isIntersecting }]) => {
        if (isIntersecting) {
            isVisible.value = isIntersecting;
            stop();
        }
    },
    {
        rootMargin: props.rootMargin,
    }
);
</script>

Component where I use intersection observer:

<ItemObserver v-slot="{ isVisible }">
    <div :class="isVisible ? 'fade-in' : 'invisible'">
        <CarouselContent>
            <CarouselItem v-for="item in 8" :key="item">
                <NuxtLink
                    to="/">
                    Link
                </NuxtLink>
            </CarouselItem>
        </CarouselContent>
    </div>
</ItemObserver>

css:

@keyframes fadeIn {
    from {
        visibility: hidden;
        opacity: 0;
    }
    to {
        visibility: visible;
        opacity: 1;
    }
}

.fade-in {
    visibility: hidden;
    opacity: 0;
    animation: fadeIn 0.3s cubic-bezier(0.5, 0, 0.5, 1) forwards;
}
.invisible {
    visibility: hidden;
    opacity: 0;
}

Looking for a possible solution.


Solution

  • The issue was in Js executing before the DOM is ready. I solved it by putting the intersection observer logic in onMounted and setting a delay of >= 0 to push task to the event loop:

    onMounted(() => {
        setTimeout(() => {
            if (!observerRef.value) return;
    
            const { stop } = useIntersectionObserver(
                observerRef,
                ([{ isIntersecting }]) => {
                    if (isIntersecting) {
                        isVisible.value = isIntersecting;
                        stop();
                    }
                },
                {
                    rootMargin: props.rootMargin,
                }
            );
        }, 0);
    });