Video

How to Add GSAP SplitText to a Timeline

Blog author Casey Lewis CL Creative
Casey Lewis
May 13, 2025
How to Add GSAP SplitText to a Timeline

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.
  • 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:
    1. define the split tween
    2. .add() it into heroTl
    3. 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.

Claim Your Design Spot Today

We dedicate our full attention and expertise to a select few projects each month, ensuring personalized service and results.

Web design portfolio