Jump to content
Search Community

Complex Scroll-Triggered Animations

Ritank Jaikar

Go to solution Solved by Cassie,

Recommended Posts

Ritank Jaikar
Posted (edited)

User has attempted to build a multi-state scroll-triggered animation, by building out a state switcher.


Example code from a prototype I was working on:

let currentState = -1;
let isTransitioning = false;
const TRANSITION_LOCK_DURATION = 900;
const switcherTimeline = gsap.timeline({ paused: true });

ScrollTrigger.create({
  trigger: ".section",
  start: "top center",
  end: "bottom bottom",
  onUpdate: (self) => {
    const progress = self.progress;

    // Map progress to states
    const newState = Math.min(
      Math.floor((progress / (1 - threshold)) * totalStates),
      totalStates - 1
    );

    if (newState !== currentState) {
      // Handle fast scroll
      if (isTransitioning) {
        switcherTimeline.progress(1, false);
      }

      isTransitioning = true;
      transition(newState, newState > currentState ? 1 : -1);
      currentState = newState;

      setTimeout(() => {
        isTransitioning = false;
      }, TRANSITION_LOCK_DURATION);
    }
  }
});

function transition(targetIndex, direction) {
  switcherTimeline.clear();
  switcherTimeline.play(0);

  const isForward = direction === 1;

  // Exit previous state
  switcherTimeline.to(prevElements, {
    opacity: 0,
    y: isForward ? -40 : 40,
    duration: 0.35,
    ease: "power2.out",
    overwrite: "auto"
  }, 0);

  // Enter next state
  switcherTimeline.fromTo(
    nextElements,
    { opacity: 0, y: isForward ? 100 : 40 },
    {
      opacity: 1,
      y: 0,
      duration: 0.7,
      delay: 0.15,
      ease: "power4.out",
      overwrite: "auto"
    },
    0.1
  );
}

Full Code:https://github.com/RitankJaikar/Learn-GSAP/tree/main/tests-and-mockups/gsap-scroll-switcher

Proposes a dedicated triggered timeline API that handles all of this declaratively:

 

gsap.scrollTimeline(".section", {
  mode: "triggered",     // not scrubbed, fires instantly at trigger points
  reverse: true,         // reverse animation when scrolling back
  lock: true             // prevent overlapping animations on fast scroll
})
.state("state1", { at: 0 })      // at: 0 = fires at the very start of the section
  .to(text1, { opacity: 1, y: 0, duration: 0.7, ease: "power4.out" })
  .to(line1, { x: "0%", duration: 0.55 }, "<")

.state("state2", { at: 0.33 })   // custom breakpoint, fires 33% through the scroll distance
  .from(text2, { opacity: 0, y: 100 })
  .to(line2, { x: "0%" }, "<")

.state("state3", { at: 0.66 })   // fires 66% through, leaving state3 to run the final third
  .from(text3, { opacity: 0, y: 100 })
  .to(line3, { x: "0%" }, "<");

// "at" is optional per state. omit it and the API falls back to
// dividing the scroll distance equally across all defined states

 

What this hypothetical API would handle automatically:

  • Trigger point calculation — explicit at values let you set custom breakpoints (as shown above), or omit at entirely and let the API divide scroll distance equally between states
  • Reverse behavior — automatically reverse each state's timeline when scrolling back
  • Fast scroll locking — complete current animation before starting next
  • Direction-aware from/to — different entry/exit values for forward vs reverse
  • Isolated execution — no interference with other GSAP animations on the page

 

 

Edited by Cassie
edited to remove inaccurate information
  • Like 1
  • Solution
Posted

Hey Ritank!

 

Thanks for the suggestion. I like the hypothetical API. However - you can actually do all of this already using separate ScrollTriggers, maybe that's the part you were missing? It's a little more code, but only a little.

 

Here's a demo. (apologies for the wonky styling, I tried to copy your filed across but some of it's a bit broken)

See the Pen c59ee970b8f3d25277a85b0925ba4cef?editors=0010 by GreenSock (@GreenSock) on CodePen.



Also in response to these points.

Quote

toggleActions is per-animation, not timeline-level. 

ToggleActions can be used on a timeline, it's not just a tween setting. You can set toggleActions on a scrollTrigger which controls a timeline. You can also create multiple scrolltriggers with different scroll points.

 

Quote

Reverse behavior (animate backwards when scrolling back up)

This is achievable with toggleActions - in fact, it's exactly what toggleActions does!

 

Quote

State locking (prevent overlapping animations on fast scroll)

We have two features here for exactly this. PreventOverlaps and fastScrollEnd

https://gsap.com/docs/v3/Plugins/ScrollTrigger/#preventOverlaps

https://gsap.com/docs/v3/Plugins/ScrollTrigger/#fastScrollEnd

 

Quote

Direction-aware transitions (different animation for forward vs reverse)

If you need different animations for forward and reverse - you can use callbacks in a timeline and fire off dynamic tweens and read direction with this property https://gsap.com/docs/v3/Plugins/ScrollTrigger/direction

 

 

Hope this helps!

 

  • Like 2
Ritank Jaikar
Posted

Hey Cassie,

 

Thanks for the detailed reply and the demo, really appreciate you taking the time to rebuild it.

 

I tried it out properly (stress tested with fast scroll, slow scroll, and reverse scroll) and wanted to share what I found, since I think it's a genuinely interesting edge case rather than something obviously wrong with the approach.

 

When scrolling straight through, start to end, forward or reverse, it works great. But when I scroll partway into a state's zone and then reverse direction before reaching the end of that 200px zone, I get overlapping animations. Two states end up partially visible at once. Attaching a screenshot of what that looks like when it happens.

 

I dug into why: toggleActions: "play none reverse none" maps "reverse" to onEnterBack, which only fires when the scroll crosses the end boundary going backward. If you scroll into a zone but reverse before reaching that end point, onEnterBack never fires for that trigger, so the tween that already started playing just keeps running to completion in the background. Meanwhile you've scrolled back into the previous zone, which fires its own onEnterBack and starts reversing that one. Now two tweens are animating the same elements in opposite directions at the same time.

 

preventOverlaps helped reduce it but didn't fully eliminate it in my testing, small back-and-forth scrolling (which happens a lot with trackpads) seems to dodge the zone-width-dependent commit point pretty easily.

 

Do you know of a workaround for this within the ScrollTrigger/toggleActions approach, or is this just an inherent limitation of using separate triggers per state? Also, the mapper trick for finding trigger points as percentages was a nice reminder, I know mapRange exists but hadn't thought to apply it this way myself, so thanks for that, useful regardless of how this particular issue shakes out.

 

 

 

image.thumb.png.6bd216f1ecf19a29f9e016cee13e5157.png

Ritank Jaikar
Posted

Update: setting end: "+=0px" on each trigger (so start and end are the same pixel) combined with preventOverlaps: true seems to fix it. Stress tested with fast scroll, slow scroll, reverse mid-zone, and hard flicks across multiple zones, no overlap in any case so far.

 

let common = { end: "+=0px", toggleActions: "play none reverse none", preventOverlaps: true, }

 

Makes sense now, making the zone width zero removes the gap between where forward triggers and where reverse triggers, which was causing the overlap in the first place.

 

Thanks again for the demo and the pointers, genuinely learned a lot working through this and got to explore some newer ways of doing things I hadn't tried before. Will definitely try out ScrollTrigger.direction (https://gsap.com/docs/v3/Plugins/ScrollTrigger/direction) for the direction-aware transitions piece next.

 

This has been really helpful, marking as resolved. Appreciate you taking the time!

  • Like 1
Posted

Glad you got it working how you wanted!

  • Cassie changed the title to Complex Scroll-Triggered Animations

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...