Back to Blog
Back to Blog
Tutorials·GSAP·April 20, 2026·9 min read

GSAP Timeline Tutorial: Sequence Animations Like a Pro (2026)

Learn how to use gsap.timeline() to sequence animations with precision. Covers the position parameter, defaults, labels, playback control, and real-world examples.

GSAP timeline tutorial showing animation sequencing with gsap.timeline() position parameter

Estimated reading time: 10 minutes | Skill level: Beginner to Intermediate

The first time I tried to sequence multiple GSAP animations, I did it with
delay
. Five tweens, each with a manually calculated delay value. It worked. Until I needed to adjust the timing of step two. Then everything broke. There's a better way.
gsap.timeline()
is how GSAP professionals sequence animations. It gives you precise control over every step, makes timing adjustments trivial, and opens up features you simply can't get with chained delays.

In this tutorial you'll learn:

  • How to create a GSAP timeline and add tweens to it
  • The position parameter (the single most powerful concept in GSAP)
  • How to use defaults, labels, and playback control
  • A real-world card reveal example you can use today

The Problem with Using Delay

Here's what animation sequencing looks like without timelines:

gsap.to(".title", { y: 0, opacity: 1, duration: 0.6 }); gsap.to(".subtitle", { y: 0, opacity: 1, duration: 0.6, delay: 0.7 }); gsap.to(".button", { y: 0, opacity: 1, duration: 0.6, delay: 1.4 }); gsap.to(".image", { scale: 1, opacity: 1, duration: 0.8, delay: 0.3 });

This works at first. But every delay is hardcoded. If you change the title animation to 0.8s, you need to manually update the subtitle delay, the button delay, and anything else downstream. It gets messy fast.

GSAP timelines fix this completely.

Creating Your First Timeline

A timeline is a container for tweens. Everything inside it plays in sequence by default.

const tl = gsap.timeline(); tl.to(".title", { y: 0, opacity: 1, duration: 0.6 }) .to(".subtitle", { y: 0, opacity: 1, duration: 0.6 }) .to(".button", { y: 0, opacity: 1, duration: 0.6 });

Now each tween starts automatically when the previous one ends. Change the title duration and everything adjusts. No manual delay math.

You can also add each tween on its own line (the result is identical):

const tl = gsap.timeline(); tl.to(".title", { y: 0, opacity: 1, duration: 0.6 }); tl.to(".subtitle", { y: 0, opacity: 1, duration: 0.6 }); tl.to(".button", { y: 0, opacity: 1, duration: 0.6 });

Visual: PLACEHOLDER - Diagram showing three sequential tweens on a timeline bar, each starting when the previous ends

The Position Parameter

This is the part most tutorials skip. The position parameter is the third argument on any timeline tween, and it controls exactly when that tween starts.

tl.to(".element", { x: 100, duration: 1 }, position);

Here's every form it can take:

Absolute Time

tl.to(".a", { x: 100, duration: 1 }, 0); // starts at 0 seconds tl.to(".b", { y: 50, duration: 1 }, 1.5); // starts at 1.5 seconds

Relative to the Previous Tween's End

tl.to(".a", { x: 100, duration: 1 }); tl.to(".b", { y: 50, duration: 1 }, "+=0.5"); // 0.5s after .a ends tl.to(".c", { opacity: 0, duration: 1 }, "-=0.3"); // 0.3s before .b ends (overlap)
The
"-=0.3"
form is something I use constantly. Overlapping animations by a few frames feels much more natural than strict sequential playback.

Relative to the Previous Tween's Start

tl.to(".a", { x: 100, duration: 1 }); tl.to(".b", { y: 50, duration: 0.5 }, "<"); // same start time as .a tl.to(".c", { opacity: 0, duration: 1 }, "<0.2"); // 0.2s after .a starts
The
"<"
shorthand means "start when the most recently added tween starts." It's how you run animations in parallel without hardcoding absolute time values.

Visual: PLACEHOLDER - Side-by-side timeline diagrams showing sequential vs overlapping tweens using position parameter

Practical Example of the Position Parameter

Here's a card entrance with a staggered but overlapping feel:

const tl = gsap.timeline({ paused: true }); // Image scales up tl.from(".card-image", { scale: 0.9, opacity: 0, duration: 0.8, ease: "power3.out" }); // Title starts 0.2s before image finishes (feels connected) tl.from(".card-title", { y: 20, opacity: 0, duration: 0.6, ease: "power3.out" }, "-=0.2"); // Body text starts when title starts (parallel) tl.from(".card-body", { y: 20, opacity: 0, duration: 0.6, ease: "power3.out" }, "<0.1"); // Button slides in last tl.from(".card-button", { y: 10, opacity: 0, duration: 0.5, ease: "power3.out" }, "-=0.2");

Compare this to what the same thing looks like with raw delays. The timeline version is cleaner and trivial to adjust.

Timeline Defaults

If all your tweens share the same duration and easing, pass them as defaults on the timeline:

const tl = gsap.timeline({ defaults: { duration: 0.6, ease: "power3.out" } }); // Each tween inherits duration and ease tl.from(".title", { y: 30, opacity: 0 }) .from(".subtitle", { y: 20, opacity: 0 }) .from(".button", { y: 15, opacity: 0 });
Individual tweens can still override defaults. A tween with
duration: 1
will use 1s, not the default 0.6s. Defaults just set the fallback.

This is a small thing that makes a real difference when you have 10+ tweens in a timeline.

Labels

Labels mark specific points in a timeline by name. They make complex sequences readable and let you seek to specific moments.

const tl = gsap.timeline(); tl.addLabel("intro", 0); tl.from(".hero-text", { y: 40, opacity: 0, duration: 0.8 }, "intro"); tl.from(".hero-image", { scale: 0.95, opacity: 0, duration: 1 }, "intro+=0.2"); tl.addLabel("details", "+=0.5"); tl.from(".detail-1", { x: -20, opacity: 0, duration: 0.5 }, "details"); tl.from(".detail-2", { x: -20, opacity: 0, duration: 0.5 }, "details+=0.15"); tl.from(".detail-3", { x: -20, opacity: 0, duration: 0.5 }, "details+=0.3");

Now instead of magic numbers, the sequence reads like a script. You can also play from a label:

tl.play("details"); // jump to the "details" section and play

This is useful for multi-step UI flows where you need to navigate to specific states.

Playback Control

Every timeline gives you full control over playback:

const tl = gsap.timeline({ paused: true }); // create paused // Trigger on user interaction button.addEventListener("click", () => tl.play()); // Other controls tl.pause(); tl.reverse(); tl.restart(); tl.progress(0.5); // jump to 50% tl.time(1.5); // jump to 1.5 seconds tl.kill(); // destroy it
A common pattern: create the timeline with
paused: true
, then trigger it on hover, click, or scroll. This is how most interactive animations work.
const card = document.querySelector(".card"); const tl = gsap.timeline({ paused: true }); tl.to(".card-overlay", { opacity: 1, duration: 0.4 }) .to(".card-label", { y: 0, opacity: 1, duration: 0.3 }, "-=0.1"); card.addEventListener("mouseenter", () => tl.play()); card.addEventListener("mouseleave", () => tl.reverse());
The
tl.reverse()
call on mouse leave runs the entire animation backwards. GSAP handles all the easing correctly in reverse. No separate "out" animation needed.

Visual: PLACEHOLDER - Code example showing hover animation with play/reverse using timeline

Timeline Options

A few constructor options worth knowing:

const tl = gsap.timeline({ paused: true, // start paused repeat: -1, // loop forever (-1 = infinite) yoyo: true, // alternate direction on each repeat delay: 0.5, // wait 0.5s before starting defaults: { duration: 0.5, ease: "expo.out" }, onComplete: () => console.log("done"), onStart: () => console.log("started") });
repeat
and
yoyo
on a timeline apply to the entire sequence. The whole thing repeats, not individual tweens.

Nesting Timelines

Timelines can contain other timelines. This is useful for building reusable animation modules.

function cardEntrance(card) { const tl = gsap.timeline(); tl.from(card.querySelector(".image"), { scale: 0.9, opacity: 0, duration: 0.7 }); tl.from(card.querySelector(".title"), { y: 20, opacity: 0, duration: 0.5 }, "-=0.2"); return tl; } const master = gsap.timeline(); master.add(cardEntrance(card1), 0); master.add(cardEntrance(card2), 0.3); master.add(cardEntrance(card3), 0.6);

Each card gets its own encapsulated animation, and the master timeline controls when each one starts. The position parameter still works the same way when adding child timelines.

A Real-World Example: Page Section Reveal

Here's how I'd animate a typical marketing section with a heading, subtext, and a row of feature cards:

function animateSection(section) { const tl = gsap.timeline({ scrollTrigger: { trigger: section, start: "top 75%", once: true }, defaults: { ease: "power3.out", duration: 0.7 } }); tl.from(section.querySelector(".section-label"), { y: 10, opacity: 0 }); tl.from(section.querySelector(".section-heading"), { y: 30, opacity: 0, duration: 0.9 }, "-=0.3"); tl.from(section.querySelector(".section-body"), { y: 20, opacity: 0 }, "-=0.4"); tl.from(section.querySelectorAll(".feature-card"), { y: 30, opacity: 0, stagger: 0.1 }, "-=0.3"); return tl; }

A few things to notice here:

  • once: true
    on ScrollTrigger means the animation fires once and stays. No replay on scroll up.
  • defaults
    keeps all the tweens consistent without repeating the same values.
  • The overlapping positions (
    "-=0.3"
    ,
    "-=0.4"
    ) create flow. Nothing feels abrupt.
  • stagger: 0.1
    on the cards makes them cascade without needing individual tweens.

If you're not familiar with ScrollTrigger yet, I wrote a full guide: GSAP ScrollTrigger Examples: 10 Patterns for Real Projects.

Common Mistakes

Using delay instead of the position parameter. Once you understand position, delay on individual tweens inside a timeline is almost never the right choice.

Forgetting

paused: true
on interactive timelines. If you create a hover timeline without pausing it, it will autoplay immediately on page load.

Putting ScrollTrigger on tweens inside a timeline. ScrollTrigger should go on the timeline itself, not on child tweens. Putting it inside breaks the sequencing.

// Wrong const tl = gsap.timeline(); tl.to(".box", { x: 100, scrollTrigger: { trigger: ".box" } }); // don't do this // Right const tl = gsap.timeline({ scrollTrigger: { trigger: ".container", start: "top 80%" } }); tl.to(".box", { x: 100 });

Skipping defaults when all tweens share the same ease/duration. It's not a bug, just unnecessary repetition.

Key Takeaways

  • Use
    gsap.timeline()
    to sequence animations. Delays are fragile. Timelines aren't.
  • The position parameter (
    "-=0.2"
    ,
    "<"
    ,
    0
    ) is the most powerful feature. Learn it first.
  • Pass
    defaults
    to the timeline to avoid repeating duration and ease on every tween.
  • Create timelines with
    paused: true
    for interactive animations and trigger them with
    play()
    and
    reverse()
    .
  • ScrollTrigger goes on the timeline, not on tweens inside it.

What's Next

If you're building scroll animations, the timeline + ScrollTrigger combination is where things get interesting. I covered 10 production-ready patterns in GSAP ScrollTrigger Examples.

For text animations specifically, GSAP SplitText Guide covers character and line-level reveals that work well inside timelines.

If you want to skip the setup and grab ready-to-use animations built with exactly these patterns, browse the Annnimate library. Every animation includes the full timeline code.

Written by

Julian Fella

Julian Fella

Founder

Related reading

GSAP stagger animation tutorial — animating lists and grids with timing offset
Tutorials·April 23, 2026

GSAP Stagger: Animate Lists and Grids with Rhythm (2026)

Learn how to use GSAP stagger to animate multiple elements with perfect timing. Covers basic stagger, advanced object syntax, from options, and real-world grid reveals.

Read article
Read article
GSAP ScrollTrigger tutorial showing multiple scroll animation patterns including parallax, reveals, and text effects
Tutorials·April 19, 2026

GSAP ScrollTrigger Examples: 10 Scroll Animations You Can Use Today

Ten production-ready GSAP ScrollTrigger patterns: fade reveals, parallax, text effects, SVG drawing, mask reveals, and flip animations. Copy-paste code for each.

Read article
Read article
GSAP SplitText tutorial showing character and line text animations with scroll triggers
Tutorials·April 17, 2026

GSAP Text Animation: A Practical SplitText Guide (2026)

How to animate text with GSAP SplitText. Covers chars, words, and lines with scroll-triggered reveals, stagger, and mask effects. Copy-paste examples included.

Read article
Read article

On This Page

Stay ahead of the curve

Join 1000+ creators getting animation updates. Unsubscribe anytime.

Animations
Animations
Pricing
Pricing
Blog
Blog
Showcase
Showcase
Changelog
Changelog
Roadmap
Roadmap
XLinkedInInstagram

© 2026 Annnimate · Built by Good Fella

Privacy
Privacy
Terms
Terms
Cookies
Cookies
Refund
Refund