gsap.timeline()
gsap.timeline() creates a sequenced container for multiple tweens. Each child tween starts when the previous one ends by default, but the position parameter, labels, and offsets let you overlap, gap, or schedule them anywhere on the timeline. Timelines reverse, pause, resume, and seek as a single unit, which makes them the right tool for any multi-step animation longer than a single tween.
Updated September 6, 2026
How timelines work
Create a timeline with gsap.timeline(), then chain .to(), .from(), .fromTo(), .set() calls. Each call appends a tween that starts when the previous one ends. The timeline's .play(), .pause(), .reverse(), .seek(), .timeScale() control the whole sequence.
The defaults option sets shared properties for every child tween so you don't repeat ease and duration. easeReverse: true (GSAP 3.15+) keeps the .out feel intact when the timeline plays in reverse, which solves the canonical 'open feels right, close feels wrong' problem.
One playhead, less jank
gsap.timeline() gives every tween one shared playhead, so related motion stays coordinated instead of competing. That matters for performance: the HTTP Archive Web Almanac found non-composited, jank-prone animations on roughly 40% of mobile pages, and driving transforms from one timeline keeps that work on the compositor thread.The position parameter
The second-to-last argument on any .to() / .from() / .set() is the position parameter. It controls WHERE on the timeline the tween starts relative to the previous one.
-=0.3-> start 0.3s before the previous tween ends (overlap)'<'-> start at the previous tween's start time (parallel)'<+0.2'-> start 0.2s after the previous tween's start'>'-> start when the previous tween ends (default, explicit)1.5-> start at absolute time 1.5s on the timeline'label'-> start at a named label set withtl.addLabel('label')
Labels, callbacks and seeking
A label is a named point on the timeline. Add one with tl.add('open') or tl.addLabel('open', 1.2), then use it as a position parameter for later tweens and as a target for playback: tl.play('open'), tl.seek('open'), tl.tweenTo('open'). Labels are how a menu timeline exposes 'go to the half-open state' without anyone counting seconds.
Callbacks live either on the timeline (onStart, onUpdate, onComplete, onReverseComplete) or inline with .call(fn, params, position). Use onReverseComplete to clean up after a close, and .call() when a side effect must happen at one point in the sequence rather than at the end.
tl.duration()returns the total length in seconds, useful for syncing audio or a progress bartl.progress(0.5)jumps to the middle,tl.progress()reads ittl.timeScale(2)plays at double speed, the honest way to test a slow sequence without editing durationstl.getChildren()lists every tween and nested timeline, which is what the Annnimate timeline inspector reads
Nested timelines
A timeline can contain other timelines. Write a function that builds and returns one piece of the choreography, then add() those pieces to a master timeline with position parameters. Each piece stays testable on its own and the master reads like a script.
Nested timelines inherit the master's timeScale, reverse with it, and are reverted with it inside gsap.context(). The one thing they do not inherit is defaults; set those on each child or pass them in the builder.
Use timelines for
- Multi-step open/close sequences (drawer, modal, menu)
- Hero reveals with overlapping headline, subhead, and CTA stages
- Coordinated stagger across multiple groups that share an overall rhythm
- Anything you want to reverse, scrub, or seek as a single unit
- Choreography across N elements where time relationships matter more than per-tween config
Controlling a timeline from React
Build the timeline once inside useGSAP, keep it paused, store it in a ref, and drive it from state with play() and reverse(). Rebuilding the timeline on every render is the usual cause of a menu that opens fine and closes wrong: the new timeline has no idea where the old one stopped.
reverse() plays the same tweens backwards from wherever the playhead is, so a click halfway through the open plays a matching half close. That interruptibility is the reason the Multi Flip and Magnetic Button components are built on one timeline rather than two.
Use something else when
- A single tween -
gsap.to()is shorter and clearer - Truly parallel independent tweens with no shared rhythm - separate
gsap.to()calls - Scroll-driven sequencing where position is scroll position, not time - use ScrollTrigger with
scrub: trueon a single tween or timeline - Two-state hover (in/out) where you want overwrite semantics - use
overwrite: 'auto'on individual tweens
Used in these Annnimate components
- The Multi Flip component drives its orchestrated flip sequence from a single timeline with per-card position parameters
- The Magnetic Button uses a paired open/close timeline so the rest-state animation matches the hover-state choreography
- The Mega Menu runs two timelines (open and close) with distinct easings rather than reversing one - the close needs different mechanics than the open
- The Dual Scramble timeline coordinates the underline expand with the character scramble so both finish at the same beat
See it running in production
Common questions
- When should I use a timeline vs separate tweens?
- Use a timeline when you want to treat the whole sequence as one unit (reverse it, pause it, scrub it). Use separate tweens when each one stands alone and you don't need shared playback control. The cost of starting with a timeline is near zero, so when in doubt - timeline.
- How do I reverse a timeline cleanly?
- Call
tl.reverse()and ScrollTrigger plus the timeline handle the rewind. For the timeline to feel right in reverse, seteaseReverse: trueon the timeline options (GSAP 3.15+) so eases stay .out in both directions. Without it, an .out ease played backwards becomes an .in ease, which feels sluggish. - How do I jump to a label on a timeline?
- A timeline label is reached with
tl.play('label')to play from it,tl.seek('label')to jump without playing, ortl.tweenTo('label')to animate the playhead there from wherever it is. Add labels withtl.add('label')ortl.addLabel('label', time). - How do I know how long a timeline is?
- A timeline reports its length with
tl.duration()in seconds, including overlaps from negative position parameters.tl.totalDuration()adds repeats and delays, andtl.progress()returns the playhead as a 0 to 1 fraction. - Can timelines have nested timelines?
- Yes - pass a timeline as a child of another with
.add(childTl). Useful when you want one timeline's logic encapsulated (e.g. anopenPanel(id)function returns its own timeline) and you compose them under a parent that handles the overall reveal. - Why doesn't my timeline play in React?
- Either you created it outside
useGSAP(and React threw it away on re-render), or you forgot to call.play()on a timeline withpaused: true. Build the timeline insideuseGSAP, store it in a ref, and call.play()/.reverse()from event handlers wrapped incontextSafe. - Should I rebuild a timeline on every open, or build once and reuse?
- Build once if positions are static. Rebuild if the timeline depends on dynamic values (current element widths, scroll position at the moment of open). Rebuilding is cheap; the bug is building once with stale measurements and watching the animation drift after a resize.
