Instructions

Lenis Scroll

<link rel="stylesheet" href="https://unpkg.com/lenis@1.3.15/dist/lenis.css" />
<script src="https://unpkg.com/lenis@1.3.15/dist/lenis.min.js"></script>

<script>
  const lenis = new Lenis({ duration: 1.4 });

  lenis.on('scroll', ScrollTrigger.update);
  gsap.ticker.add((time) => {
    lenis.raf(time * 1000);
  });
  gsap.ticker.lagSmoothing(0);
</script>

Image Parallax

<script>
  document.addEventListener('DOMContentLoaded', () => {
    gsap.registerPlugin(ScrollTrigger);

    const mm = gsap.matchMedia();

    mm.add(
      {
        mobile: '(max-width: 479px)',
        mobileLandscape: '(min-width: 480px) and (max-width: 767px)',
        tablet: '(min-width: 768px) and (max-width: 991px)',
        desktop: '(min-width: 992px)',
      },
      (context) => {
        const { mobile, mobileLandscape, tablet, desktop } = context.conditions;

        document.querySelectorAll('[data-parallax="trigger"]').forEach((trigger) => {
          // Disable parallax on selected breakpoints
          const disable = trigger.getAttribute('data-parallax-disable');

          if ((disable === 'mobile' && mobile) || (disable === 'mobileLandscape' && mobileLandscape) || (disable === 'tablet' && tablet) || (disable === 'desktop' && desktop)) {
            return;
          }

          // Target element
          const target = trigger.querySelector('[data-parallax="target"]') || trigger;

          // Direction
          const direction = trigger.getAttribute('data-parallax-direction') || 'vertical';

          const property = direction === 'horizontal' ? 'xPercent' : 'yPercent';

          // Parallax values
          const startValue = parseFloat(trigger.getAttribute('data-parallax-start')) || 0;

          const endValue = parseFloat(trigger.getAttribute('data-parallax-end')) || -15;

          // Scrub
          const scrubAttribute = trigger.getAttribute('data-parallax-scrub');

          const scrub = scrubAttribute !== null ? parseFloat(scrubAttribute) : true;

          // ScrollTrigger positions
          const scrollStart = trigger.getAttribute('data-parallax-scroll-start') || 'top bottom';

          const scrollEnd = trigger.getAttribute('data-parallax-scroll-end') || 'bottom top';

          gsap.fromTo(
            target,
            {
              [property]: startValue,
            },
            {
              [property]: endValue,
              ease: 'none',
              scrollTrigger: {
                trigger: trigger,
                start: `clamp(${scrollStart})`,
                end: `clamp(${scrollEnd})`,
                scrub: scrub,
                invalidateOnRefresh: true,
              },
            },
          );
        });

        ScrollTrigger.refresh();
      },
    );
  });
</script>

Text Swapping

<script>
document.addEventListener('DOMContentLoaded', () => {
  document.querySelectorAll('[data-rotating-words]').forEach((el) => {
    const words = el
      .getAttribute('data-rotating-words')
      .split(',')
      .map((word) => word.trim())
      .filter(Boolean);
    if (words.length < 2) return;
    let index = 0;
    let current;
    el.style.display = 'inline-grid';
    el.style.overflow = 'hidden';
    el.style.verticalAlign = 'bottom';
    function createWord(word) {
      const line = document.createElement('span');
      line.style.gridArea = '1 / 1';
      line.style.display = 'block';
      line.style.whiteSpace = 'nowrap';

      [...word].forEach((letter) => {
        const char = document.createElement('span');
        char.textContent = letter;
        char.style.display = 'inline-block';
        line.appendChild(char);
      });
      return line;
    }
    function showInitialWord() {
      current = createWord(words[index]);
      el.replaceChildren(current);
      gsap.set(current.children, {
        yPercent: 0,
        autoAlpha: 1,
      });
      gsap.delayedCall(2, rotateWord);
    }
    function rotateWord() {
      const nextIndex = (index + 1) % words.length;
      const next = createWord(words[nextIndex]);
      el.appendChild(next);
      // Incoming letters start below, already visible
      gsap.set(next.children, {
        yPercent: 110,
        autoAlpha: 1,
      });
      const tl = gsap.timeline({
        onComplete: () => {
          current.remove();
          current = next;
          index = nextIndex;
          gsap.delayedCall(2, rotateWord);
        },
      });
      // Old letters move up and new letters rise simultaneously
      tl.to(
        current.children,
        {
          yPercent: -110,
          duration: 0.5,
          stagger: 0.055,
          ease: 'power2.in',
        },
        0,
      );
      tl.to(
        next.children,
        {
          yPercent: 0,
          duration: 0.5,
          stagger: 0.055,
          ease: 'power3.out',
        },
        0.4,
      );
    }
    showInitialWord();
  });
});
</script>

<style>
  [data-rotating-words] {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  }
  .rotating-text__inner {
    display: inline-grid;
    overflow: hidden;
    vertical-align: bottom;
  }
  .rotating-text__word {
    grid-area: 1 / 1;
    display: block;
    white-space: nowrap;
    transform: translateY(100%);
  }
</style>