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:
- When the text actually gets split (which can happen asynchronously, especially if you use autoSplit and have custom fonts).
- How you animate the newly created pieces (lines, words, characters).
- 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.
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.