A GSAP tween with yoyo: true ends on its "from" state whenever repeat is odd. Total plays = repeat + 1, and yoyo alternates direction each play, so odd repeat means the last play runs backwards.
This is silent. No error, no warning, and in a normal auto-playing page you rarely notice because the element is usually about to be removed anyway. In a seek-based deterministic renderer (frame N = timeline.seek(N/fps)), it is a shipped bug: every frame after the tween ends renders the element in its start state.
I hit this animating letters of the word "skips" to stutter like a skipping record:
// BUG: repeat:3 + yoyo => 4 plays => ends BACKWARD, letters stay at opacity 0.2
tl.fromTo(sel, { opacity: 0.2, x: -6 },
{ opacity: 1, x: 0, duration: 0.12, ease: 'steps(1)', stagger: 0.07,
repeat: 3, yoyo: true }, 16.43);Verified empirically on GSAP 3.15.0 by tweening a plain object and seeking far past the end:
function endValue(repeat, yoyo) {
const o = { v: 0 }; // from = 0, to = 1
const tl = gsap.timeline({ paused: true });
tl.fromTo(o, { v: 0 }, { v: 1, duration: 0.1, ease: 'none', repeat, yoyo }, 0);
tl.seek(100);
return o.v;
}| repeat | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
yoyo: true | 1 | 0 | 1 | 0 | 1 | 0 |
yoyo: false | 1 | 1 | 1 | 1 | 1 | 1 |
Rules that follow:
- If a yoyo tween must end visible, use an even
repeat(repeat + 1plays, ending forward). - Better for entrances: drop
yoyoentirely. A stagger with a stepped ease gives the same stutter and cannot land wrong. That is what shipped:tl.fromTo(sel, { opacity: 0.2, x: -6 }, { opacity: 1, x: 0, duration: 0.14, ease: 'steps(2)', stagger: 0.06 }, 16.43); - Reserve
yoyofor things that genuinely return to rest (a pulse on an element whose resting state IS the "from" value), never for a reveal.
Related trap in the same family: fromTo defaults to immediateRender: true, so on a paused/seeked timeline the element sits in its "from" state from time 0 until the tween starts. Useful for hiding things pre-cue, but it means a card can look empty for seconds if you schedule its inner reveal too late - check a frame early in each element's visible window, not just at its animation cue.