#1167·anime

[BUG] onScroll autoplay animations aren't working with timelines after timeline sync.

Author: rayyanekaputraCreated Apr 25, 2026Updated Jun 9, 2026
Labelsbug

Describe the bug

onScroll() events aren't firing the animation when inview just because it is inserted on a timeline.

Provide a minimal reproduction demo of the bug

the box we want to animate. in its own, its working:

const box = animate(utils.$(".box"), {
			autoplay: onScroll({
		debug:true,
		enter: "bottom start",
		leave: "start bottom",
		sync: "resume pause reverse reset", //so when inview, it alternates
		onEnter: () => {
			console.log("FIRING AND THE ANIMATION ALSO PLAYS AFTER A FIRE ");
		},
	}),
	x: "-2em",
	ease: "inOutExpo",

});

but, if we put this inside of a timeline. we need to:

  1. explicitly tell the scrollObserver that we are targeting the current element we are animating.
  2. cant use sync property inside autoplay, which means the animation can't alternate based on scroll events. (dont get confused of the alternate property with loop, thats not what i meant.)

putting the .box animation inside of a timeline then .sync() with other timelines:

const boxTimeline = createTimeline()
boxTimeline.add(utils.$(".box"), {
	autoplay: onScroll({
		debug: true,
		target: utils.$(".box"),
		enter: "bottom start",
		leave: "start bottom",
		sync: "resume pause reverse reset", //no longer alternates!
		onEnter: () => {
			console.log("FIRING BUT IT AINT PLAYING ");
		},
	}),
	x: "-2em",
	ease: "inOutExpo",
});
//master timeline -> main control
const masterTimeline = createTimeline();
masterTimeline
	.sync(introTimeline)
	.sync(heroTimeline, "1050")
	.sync(boxTimeline) //with the intention that it only plays after all these timelines finished

Alternatives/Working Solutions

First option

single them out as a standalone animation, not a timeline. then you sync() them. this will play from start. maybe scrollObserver have nothing to do with sync(). i didnt know yet.

const box = animate(utils.$(".box"), {
		autoplay: onScroll({
		debug:true,
		enter: "bottom start",
		leave: "start bottom",
		sync: "resume pause reverse reset", //so when inview, it alternates
		onEnter: () => {
			console.log("FIRING AND THE ANIMATION ALSO PLAYS AFTER A FIRE ");
		},
	}),
	x: "-2em",
	ease: "inOutExpo",
});
const masterTimeline = createTimeline();
masterTimeline
	.sync(introTimeline)
	.sync(heroTimeline, "1050")
	.sync(box)  //this will play EVEN IF the other timelines haven't finished. i have no idea why yet

Second option (what i use)

single them out as a standalone animation, not a timeline. then you call() them. this will play exclusively after the call() is initiated.

const masterTimeline = createTimeline();
masterTimeline.sync(introTimeline).sync(heroTimeline, "1050").call(() => { //it only creates this animation AFTER the whole timeline finished, sorted by the method chaining.
			animate(utils.$(".box"), {
				autoplay: onScroll({
					debug: true,
					enter: "bottom start",
					leave: "start bottom",
					sync: "resume pause reverse reset", //alternates!
					onEnter: () => {
						console.log("FIRING ONLY AFTER OTHER TIMELINES FINISHED, IT ALTERNATES TOO ");
					},
				}),
				x: "-2em",
				ease: "inOutExpo",
				duration: 1000,
			});
		});

Conclusion

if this is actually intentional, the official docs didn't mention anything/show any example on having autoplay NOT as a createTimeline() property. the mindset is so that each animation is observed inside a single timeline.

from the official docs on using onScroll() on a timeline.

// Timeline

const circles = utils.$('.circle');

createTimeline({
  alternate: true,
  loop: true,
  autoplay: onScroll({ //only on timeline level, not a single animate
    target: circles[0],
    container,
    debug
  })
})
.add(circles[2], { x: '9rem' })
.add(circles[1], { x: '9rem' })
.add(circles[0], { x: '9rem' });

also if this intentional for the best practice to be only declared on the timeline level, then it should be logical to have the target property to accept array/nodelist/htmlcollections. i was thinking like:

// Timeline

const circles = utils.$('.circle');
const box = utils.$('.box');
const tri = utils.$('.tri);


createTimeline({
  alternate: true,
  loop: true,
  autoplay: onScroll({ 
    target: [circles, box, tri], //something like this, now it make sense to have 1 master scrollObserver
    container,
    debug
  })
})
.add(circles[2], { x: '9rem' })
.add(box, { x: '9rem' })
.add(tri, { x: '9rem' });

Thank You

i love anime.js, been using it for years! but i keep doing hacky things for some interactions to work natively so now i try to contribute