Skip to main content

May 13, 2025

How to Add GSAP SplitText to a Timeline

5 min read

GSAP's SplitText plugin is a powerful tool for creating engaging text animations, but getting it to work smoothly with timelines can be tricky.

In this comprehensive guide, we'll explore how to properly implement SplitText animations, both standalone and within GSAP timelines. Whether you're building hero sections, animated headings, or complex text transitions, understanding these patterns will help you create more reliable and maintainable animations.

We'll cover everything from basic implementation to advanced timeline integration, complete with code examples and best practices. Let's get started!

Implement a SplitText animation not in a timeline

Let's explore how to implement basic SplitText animations without a timeline. This simpler approach is perfect for straightforward text reveal effects.

The following example demonstrates a basic implementation that splits text into words and lines, then animates them with a staggered reveal effect.

First, here's the HTML structure you'll need:

<div class="home-hero_heading">
  Your heading text goes here
</div>

The JavaScript code below handles the text splitting and animation:

gsap.registerPlugin(SplitText) 

document.fonts.ready.then(() => {
  gsap.set(".home-hero_heading", { opacity: 1});
  
  let split;
  SplitText.create(".home-hero_heading", {
    type: "words, lines",
    linesClass: "line",
    autoSplit: true,
    mask: "lines",
    onSplit: (self) => {
      split = gsap.from(self.lines,{
        duration: 0.6,
        yPercent: 100,
        opacity: 0,
        stagger: 0.1,
        ease: "expo.out",
      });
      return split;
    }
  });
});

Let's break down what this code does:

  • gsap.registerPlugin(SplitText) - Registers the SplitText plugin for use
  • document.fonts.ready - Ensures custom fonts are loaded before splitting text
  • gsap.set() - Makes the heading visible after fonts load
  • SplitText.create() - Splits the text into words and lines
  • onSplit callback - Handles the animation once splitting is complete

This code is adapted from the official GSAP SplitText documentation: https://gsap.com/docs/v3/Plugins/SplitText/

CSS to remove flash of content on load

/* 1b. Your heading wrapper: hide it completely */
.home-hero_heading {
  opacity: 0;
}

Now that we understand how to implement basic SplitText animations, let's explore how to integrate them into a GSAP timeline. This approach gives you more control and allows you to coordinate text animations with other elements on your page.

Working with timelines requires a slightly different mindset and implementation strategy. We'll need to consider not just the animation itself, but also how it fits into the broader sequence of events in your timeline.

Add SplitText to a timeline

When you work with SplitText and GSAP timelines together, you’re really juggling three things:

  1. When the text actually gets split (which can happen asynchronously, especially if you use autoSplit and have custom fonts).
  2. How you animate the newly created pieces (lines, words, characters).
  3. How you sequence those animations into your master timeline so everything plays in order.

Declaring a split variable up front, then assigning to it inside the onSplit callback, and finally returning it—here’s why each step matters:

1. You need to wait until the text is actually split

SplitText.create(".home-hero_heading", {
  …,
  onSplit: (self) => {
    // ← this only fires after the DOM has been wrapped in your line/word elements
  }
});
  • SplitText doesn’t split until you call SplitText.create().
  • If you’re using autoSplit: true (or waiting on document.fonts.ready), that split can happen after your initial code runs.
  • So you put your animation logic in the onSplit callback—that guarantees that self.lines (or self.words/self.chars) actually exist in the DOM before you try to animate them.

2. Capturing the tween lets you sequence and control it

let split;
SplitText.create(".home-hero_heading", {
  …,
  onSplit: (self) => {
    split = gsap.from(self.lines, { /* your line-reveal animation */ });
    heroTl.add(split, "-=0.3");
    return split;
  }
});
  • By assigning your gsap.from(...) call to the outer split variable, you give yourself a handle on that tween:You can easily kill it or reverse it later if you need to.You can inspect its progress, change its duration, etc., at runtime.
  • You can easily kill it or reverse it later if you need to.
  • You can inspect its progress, change its duration, etc., at runtime.
  • Returning that tween from onSplit is also important because SplitText will automatically clean up (kill) the returned tween if it ever has to revert and re-split (for example, if your container’s width changes or fonts finish loading).

3. Adding it to your timeline keeps everything in sync

// earlier in your code you already created your main heroTl:
const heroTl = gsap.timeline({ defaults: { ease: "power2.out" } });

// then later, once SplitText does its magic:
heroTl
  .to(".home-hero_image", { /* first animation */ })
  .set(".home-hero_heading", { opacity: 1 }, "-=0.2")
  .to(".home-hero_eyebrow", { /* next */ })

// …and finally inside onSplit:
heroTl.add(split, "-=0.3")
      .to(".home-hero_content-wrapper", { opacity: 1, y: 0 }, "-=1");
  • If you just fired gsap.from(self.lines) on its own, you’d get an animation—but it wouldn’t live inside your heroTl sequence.
  • By capturing it (split = …) and then doing heroTl.add(split, offset), you ensure that your split-text animation plays exactly when you want relative to the rest of the timeline.

Example Pattern

Here's a complete example that ties together everything we've discussed about implementing SplitText with GSAP timelines. This pattern demonstrates:

  • How to properly sequence animations before and after text splitting
  • Clean organization of timeline tweens
  • Proper handling of the split text animation within the timeline
  • CSS structure to prevent flash of unstyled content

The code below shows a hero section animation where an image slides in, text splits and reveals line by line, followed by content fading in - all choreographed in a single timeline.

gsap.registerPlugin(SplitText);

document.fonts.ready.then(() => {
  let split;                                    // 1. declare in outer scope

  const heroTl = gsap.timeline({
    defaults: { ease: "power2.out" }
  });

  // 2. Pre-split animations
  heroTl
    .to(".home-hero_image", { clipPath: "inset(0 0 0 0%)", opacity: 1, duration: 1.2 })
    .set(".home-hero_heading", { opacity: 1 }, "-=0.2") //Make sure to include a .set to reset the opacity if you hide it with CSS
    .to(".heading-style-eyebrow", { opacity: 1, duration: 0.6 });

  // 3. Split + post-split in one callback
  SplitText.create(".home-hero_heading", {
    type: "words, lines",
    linesClass: "line",
    autoSplit: true,
    mask: "lines",
    onSplit: self => {
      // 3a. Build your split-text tween
      split = gsap.from(self.lines, {
        duration: 1.5,
        yPercent: 100,
        opacity: 0,
        stagger: 0.1,
        ease: "expo.out"
      });

      // 3b. Insert it into heroTl, then chain the next tween
      heroTl
        .add(split, "-=0.3")                              // play split a bit before the previous tween ends
        .to(".home-hero_content-wrapper", {
          duration: 0.8,
          opacity: 1,
          y: 0
        }, "-=1");                                         // overlap content reveal with tail of split

      return split;                                        // allows SplitText to manage/resplit if needed
    }
  });
});

CSS that goes with Example Pattern

.home-hero_image-wrapper .home-hero_image {
  clip-path: inset(0 0 0 100%);
  -webkit-clip-path: inset(0 0 0 100%);
  opacity: 0;
  will-change: clip-path;
}

/* 3) Hide the eyebrow line */
.home-hero_heading-wrapper .heading-style-eyebrow {
  opacity: 0;
}

/* 1b. Your heading wrapper: hide it completely */
.home-hero_heading {
  opacity: 0;
}

/* 5) Hide the content wrapper */
.home-hero_content-wrapper {
  opacity: 0;
  transform: translateY(20px);
  will-change: transform, opacity;
}

Why this feels odd at first

  • You’re used to building a timeline in one linear chain, but here onSplit breaks that chain into two pieces.
  • Anything that must run after the split-text animation has to live inside onSplit, because that’s literally when your split elements exist and can be animated.

Tips for clarity

  • Keep your heroTl declarations together (all your .to(), .set(), but stop just before the split).
  • Use onSplit purely to:define the split tween.add() it into heroTlchain your post-split tweens
  • define the split tween
  • .add() it into heroTl
  • chain your post-split tweens
  • Return the split tween so SplitText can tear down or re-split cleanly if the DOM/layout changes.

Following this pattern lets you keep the “pre-split” and “post-split” logic semantically separated, while still driving one coherent timeline.

In summary

  • Declare let split out front so you have a variable in scope.
  • Animate your split elements inside the onSplit callback—this guarantees the DOM is ready.
  • Assign that tween to split so you can reference and control it later.
  • Return it from onSplit so SplitText can manage cleanup/re-splitting under the hood.
  • Add it into your master timeline (heroTl.add(split, …)) so everything stays in perfect sequence.

Taken together, that pattern ensures your text-splitting is robust (fonts/loading won’t break it), your split animations are fully controllable, and they’re neatly choreographed within your overall GSAP timeline.

marketing health check clipboard showing score

Your Marketing Isn't Bringing in Leads and You Can't Figure Out Why

Score your marketing across 7 key areas in 5 minutes and find out exactly what's holding back your leads and what to fix first.

Questions You Might Be Asking

SplitText used to be a Club GreenSock plugin, but as of GSAP 3.13 it's part of the free open-source package. You can load it directly from the CDN or install via npm without a Club membership. If you're on an older GSAP version (3.12 or below), you'll still need the Club license to access it. Upgrade to 3.13+ and the gating disappears.

SplitText runs after the DOM loads, so there's a moment where the full text is visible before it gets broken up. Fix this by hiding the target element in CSS with opacity: 0 or visibility: hidden, then using gsap.set() inside document.fonts.ready to reveal it after the split. This prevents the flash of unstyled content regardless of network conditions.

Chars split every letter into its own element — dramatic but expensive on long text and harder for screen readers. Words split each word into a span — good for staggered reveals. Lines split visual lines based on how text wraps — smoothest for hero headings because each line animates as a unit. Pick the smallest unit that achieves your effect; fewer elements means smoother performance.

SplitText 3.13+ has autoSplit: true which handles re-splitting automatically when text wraps differently at new viewport widths. For older versions, call split.revert() then re-run SplitText.create() inside a resize handler, debounced to fire once per resize end. Keep your animations inside the onSplit callback so they re-apply correctly after each new split.

Stop losing deals to a website that's holding you back.

Book a 30-minute strategy session. You'll leave with a clearer picture of what your site needs to do and a rough budget range, whether you hire us or not.

background cta image collage of websites

Other posts in this category

video thumbnail showcasing video and how to build trust with testimonials in the background

How to build trust quickly with potential clients

Building trust with potential clients is something that will be the difference between making a sale or not. Here's how you can build trust quickly.

A three-way comparison graphic: three website mockups side by side labeled DIY Builder, Template, and Custom, each with a small price tag and a "best when" note beneath it, and an arrow underneath showing the progression as a business grows.

Is a Custom Website Worth It, or Should I Use a Template or DIY Builder?

Custom, template, or DIY builder? Here's how the three options compare, plus a simple decision framework.

confetti over Vimeo Video playing

How to Add a Confetti Animation When a Vimeo Video Starts Playing

Learn how to trigger a canvas confetti animation when a Vimeo video starts playing using the Vimeo Player API play event