Search code examples
vue.jsnuxt.jsgsapscrolltriggerlenis

Mixing Scrolltrigger with Lenis in a Nuxt project


I am having some troubles making a scrolltrigger animation in a nuxt component.

Basically I have a lenis script for a nuxt page and inside this page a scrolltrigger script for the projectItem.vue component. I installed gsap via npm and imported it both in the page and the component.

The code seems to work because we enter in the onEnter() callback but there is no animation at all.

<script>
import { gsap } from "gsap";
import ScrollTrigger from 'gsap/ScrollTrigger';

export default {

  mounted() {
    gsap.registerPlugin(ScrollTrigger);
    const trigger = this.$refs.animtrigger; 
    console.log(trigger);
    gsap.to(trigger, {
      scrollTrigger: {
        trigger: trigger, 
        start: 'top center', 
        y: '-=100px', 
        ease: "power1.inOut",
        stagger: .2,
        onEnter: () => {
          console.log('coucou')
        }
      }
    })
  },

  props: {
  titre: String,
  type: String,
  composition: Array,
  outils: Array,
  date: String,
  image: String,
  alt: String
  }

}

</script>

<template>
  <div class="projectItem" ref="animtrigger">
    <div class="column">
      <div class="column-c-image">
        <img :src="(image)" :alt="(alt)" class="image">
      </div>
      <div class="column-c-content">
        <p>{{titre}}</p>
      <svg class="arrow" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path d="M6.22438 17.9287C6.36591 18.0667 6.55676 18.1423 6.75438 18.1387C6.95141 18.1391 7.14109 18.064 7.28438 17.9287L16.6144 8.58994V16.6487C16.6144 17.0629 16.9502 17.3987 17.3644 17.3987C17.5658 17.4042 17.7608 17.3273 17.9042 17.1857C18.0477 17.0442 18.1272 16.8502 18.1244 16.6487V6.9165C18.1504 6.80241 18.1498 6.68188 18.1198 6.56444C18.0519 6.29871 17.8444 6.09121 17.5787 6.02334C17.4612 5.99335 17.3407 5.99275 17.2266 6.01873H7.49438C7.08017 6.01873 6.74438 6.35451 6.74438 6.76873C6.74438 7.18294 7.08017 7.51873 7.49438 7.51873H15.5644L6.22438 16.8587C6.08085 16.9997 6 17.1925 6 17.3937C6 17.5949 6.08085 17.7877 6.22438 17.9287Z" fill="black"/>
      </svg>
      </div>
    </div>
  </div>
</template>

<style scoped lang="scss">

@use "~/assets/css/main.scss";
  .projectItem {
    width: 628px;

    .column {
      display: flex;
      flex-direction: column;
      gap: .2rem;

      .column-c-image {
        object-fit: cover;

        .image {
          width: 100%;
          height: 700px;
          object-fit: cover;
          border-radius: 1rem;

        }
      }

      .column-c-content {
        display: flex;
        flex-direction: row;
        justify-content: space-between;
        align-items: center;
        width: 100%;
      }
    }
  }


</style>



Thank you !


Solution

  • Using gsap.timeline() is recommended if you need to load multiple images like this.

    const timeline = gsap.timeline({
      scrollTrigger: {
        start: 'top center', 
        toggleActions: "play none none reverse",
      },
      stagger: {
        each: 0.2,
        from: 'left',
      },
    });
    
    timeline.from(trigger, {
      y: -100,
      opacity: 0,
      ease: "power1.inOut",
    });
    

    Since GSAP is not Vue-based, you don't necessarily have to stick to passing Vue elements. If you're sure that the DOM has loaded, which can be assumed in the mounted() hook, you can pass a native JS selector to the to() function as well, as this will result in just one animation. To insert delays between multiple animations, you need to pass many elements to one animation.

    gsap.registerPlugin(ScrollTrigger);
    
    gsap.to('.projectItem', {
      scrollTrigger: {
        start: 'top center',
        toggleActions: "play none none reverse",
      },
      y: -100,
      opacity: 1,
      ease: "power1.inOut",
      stagger: {
        each: 0.2,
        from: 'left',
      },
    });
    

    I made an inline CDN example. When the image enters the visible area, it appears, and when it leaves the visible area, it disappears.

    Example # 1

    const { createApp } = Vue;
    
    const GsapExample = {
      props: {
        titre: String,
        type: String,
        composition: Array,
        outils: Array,
        date: String,
        image: String,
        alt: String,
      },
      template: `
        <div class="projectItem" ref="animtrigger">
          <div class="column">
            <div class="column-c-image">
              <img :src="(image)" :alt="(alt)" class="image">
            </div>
            <div class="column-c-content">
              <p>{{ titre }}</p>
            <svg class="arrow" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
              <path d="M6.22438 17.9287C6.36591 18.0667 6.55676 18.1423 6.75438 18.1387C6.95141 18.1391 7.14109 18.064 7.28438 17.9287L16.6144 8.58994V16.6487C16.6144 17.0629 16.9502 17.3987 17.3644 17.3987C17.5658 17.4042 17.7608 17.3273 17.9042 17.1857C18.0477 17.0442 18.1272 16.8502 18.1244 16.6487V6.9165C18.1504 6.80241 18.1498 6.68188 18.1198 6.56444C18.0519 6.29871 17.8444 6.09121 17.5787 6.02334C17.4612 5.99335 17.3407 5.99275 17.2266 6.01873H7.49438C7.08017 6.01873 6.74438 6.35451 6.74438 6.76873C6.74438 7.18294 7.08017 7.51873 7.49438 7.51873H15.5644L6.22438 16.8587C6.08085 16.9997 6 17.1925 6 17.3937C6 17.5949 6.08085 17.7877 6.22438 17.9287Z" fill="black"/>
            </svg>
            </div>
          </div>
        </div>
      `,
    };
      
    const app = createApp({
      components: {
        GsapExample,
      },
      mounted() {
        gsap.registerPlugin(ScrollTrigger);
    
        gsap.to('.projectItem', {
          scrollTrigger: {
            start: 'top center',
            toggleActions: "play none none reverse",
          },
          y: -100,
          opacity: 1,
          ease: "power1.inOut",
          stagger: {
            each: 0.2,
            from: 'left',
          },
        });
      },
    }).mount('#app');
    .container {
      margin-top: 300px; /* no need it, just added it for testing the appearance and disappearance */
      display: flex;
      flex-wrap: wrap;
      gap: 50px;
    }
    
    .projectItem {
      width: 200px;
      opacity: 0;
    }
    
    .projectItem .column {
      display: flex;
      flex-direction: column;
      gap: 0.2rem;
    }
    
    .projectItem .column .column-c-image {
      object-fit: cover;
    }
    
    .projectItem .column .column-c-image .image {
      width: 100%;
      height: 200px;
      object-fit: cover;
      border-radius: 1rem;
    }
    
    .projectItem .column .column-c-content {
      display: flex;
      flex-direction: row;
      justify-content: space-between;
      align-items: center;
      width: 100%;
    }
    <script src="https://unpkg.com/[email protected]/dist/vue.global.prod.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
    
    <div id="app">
      <div class="container">
        <gsap-example
          v-for="number of [1, 2, 3, 4, 5, 6, 7, 8, 9]"
          titre="Title"
          type="Type"
          :composition="['Composition1', 'Composition2']"
          :outils="['Tool1', 'Tool2']"
          date="2024-04-24"
          :image="`https://picsum.photos/200/200?${number}`"
          alt="Image Alt"
        />
      </div>
    </div>

    Example # 2

    const { createApp } = Vue;
    
    const GsapExample = {
      props: {
        titre: String,
        type: String,
        composition: Array,
        outils: Array,
        date: String,
        image: String,
        alt: String,
      },
      template: `
        <div class="projectItem" ref="animtrigger">
          <div class="column">
            <div class="column-c-image">
              <img :src="(image)" :alt="(alt)" class="image">
            </div>
            <div class="column-c-content">
              <p>{{ titre }}</p>
            <svg class="arrow" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
              <path d="M6.22438 17.9287C6.36591 18.0667 6.55676 18.1423 6.75438 18.1387C6.95141 18.1391 7.14109 18.064 7.28438 17.9287L16.6144 8.58994V16.6487C16.6144 17.0629 16.9502 17.3987 17.3644 17.3987C17.5658 17.4042 17.7608 17.3273 17.9042 17.1857C18.0477 17.0442 18.1272 16.8502 18.1244 16.6487V6.9165C18.1504 6.80241 18.1498 6.68188 18.1198 6.56444C18.0519 6.29871 17.8444 6.09121 17.5787 6.02334C17.4612 5.99335 17.3407 5.99275 17.2266 6.01873H7.49438C7.08017 6.01873 6.74438 6.35451 6.74438 6.76873C6.74438 7.18294 7.08017 7.51873 7.49438 7.51873H15.5644L6.22438 16.8587C6.08085 16.9997 6 17.1925 6 17.3937C6 17.5949 6.08085 17.7877 6.22438 17.9287Z" fill="black"/>
            </svg>
            </div>
          </div>
        </div>
      `,
    };
      
    const app = createApp({
      components: {
        GsapExample,
      },
      mounted() {
        gsap.registerPlugin(ScrollTrigger);
        
        // only need to create the timeline once
        const timeline = gsap.timeline({
          scrollTrigger: {
            start: 'top center', 
            toggleActions: "play none none reverse",
          },
          stagger: {
            each: 0.2,
            from: 'left',
          },
        });
    
        const triggers = document.querySelectorAll('.projectItem');
        triggers.forEach((trigger, index) => {
          timeline.from(trigger, {
            y: -100,
            opacity: 0,
            ease: "power1.inOut",
          });
        });
      },
    }).mount('#app');
    .container {
      margin-top: 300px; /* no need it, just added it for testing the appearance and disappearance */
      display: flex;
      flex-wrap: wrap;
      gap: 50px;
    }
    
    .projectItem {
      width: 200px;
    }
    
    .projectItem .column {
      display: flex;
      flex-direction: column;
      gap: 0.2rem;
    }
    
    .projectItem .column .column-c-image {
      object-fit: cover;
    }
    
    .projectItem .column .column-c-image .image {
      width: 100%;
      height: 200px;
      object-fit: cover;
      border-radius: 1rem;
    }
    
    .projectItem .column .column-c-content {
      display: flex;
      flex-direction: row;
      justify-content: space-between;
      align-items: center;
      width: 100%;
    }
    <script src="https://unpkg.com/[email protected]/dist/vue.global.prod.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
    
    <div id="app">
      <div class="container">
        <gsap-example
          v-for="number of [1, 2, 3, 4, 5, 6, 7, 8, 9]"
          titre="Title"
          type="Type"
          :composition="['Composition1', 'Composition2']"
          :outils="['Tool1', 'Tool2']"
          date="2024-04-24"
          :image="`https://picsum.photos/200/200?${number}`"
          alt="Image Alt"
        />
      </div>
    </div>