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.

Estimated reading time: 10 minutes | Skill level: Intermediate
Text animation is the thing that separates a good site from a great one. A headline that just sits there is wasted space. A headline that reveals itself as you read it, character by character, makes people stop scrolling.
The problem is most text animation tutorials either show you Framer Motion (not great for complex sequences) or raw CSS keyframes (painful to maintain). The right tool for this is GSAP SplitText. It gives you character, word, and line-level control. No splitting logic to write yourself. No wrapping divs by hand.
And as of GSAP 3.13, SplitText is completely free. No club membership required.
This guide covers the patterns I actually use in production. Working code included.
What SplitText Does
SplitText takes a text node and wraps each character, word, or line in a<div><span>Without SplitText, you can animate the whole element as one unit. With it, you can stagger each character, clip individual lines, or sequence words with precise timing.
One line of code:
const split = SplitText.create(".headline", { type: "chars,words,lines" });
split.charssplit.wordssplit.linesSetup
Install GSAP:
npm install gsap
Register SplitText in your JavaScript:
import { gsap } from "gsap";
import { SplitText } from "gsap/SplitText";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(SplitText, ScrollTrigger);
useGSAPimport { gsap } from "gsap";
import { SplitText } from "gsap/SplitText";
import { useGSAP } from "@gsap/react";
import { useRef } from "react";
if (typeof window !== "undefined") {
gsap.registerPlugin(SplitText);
}
One important note: wait for fonts to load before splitting. If you split before the font renders, the character positions will be wrong.
useEffect(() => {
document.fonts.ready.then(() => setFontsLoaded(true));
}, []);
Pattern 1: Character Reveal on Scroll
The most common pattern. Each character fades and slides up as the element enters the viewport.
useGSAP(() => {
if (!fontsLoaded) return;
const split = SplitText.create(headlineRef.current, { type: "chars" });
gsap.from(split.chars, {
opacity: 0,
y: 40,
duration: 0.6,
stagger: 0.02,
ease: "expo.out",
scrollTrigger: {
trigger: headlineRef.current,
start: "top 85%",
once: true,
},
});
return () => split.revert();
}, { scope: containerRef, dependencies: [fontsLoaded] });
stagger: 0.020.010.04You can see this pattern live in the Character Appear animation.
Pattern 2: Line Mask Reveal
This is the one you see on award-level sites. Each line of text slides up from behind a clipping container. The text appears to emerge from nothing.
The trick is splitting by lines and settingoverflow: hiddenuseGSAP(() => {
if (!fontsLoaded) return;
const split = SplitText.create(textRef.current, {
type: "lines",
linesClass: "line-wrapper",
});
gsap.set(".line-wrapper", { overflow: "hidden" });
gsap.from(split.lines, {
yPercent: 110,
duration: 1.0,
stagger: 0.1,
ease: "expo.out",
scrollTrigger: {
trigger: textRef.current,
start: "top 80%",
once: true,
},
});
return () => split.revert();
}, { scope: containerRef, dependencies: [fontsLoaded] });
yPercent: 110This works best with large text (24px and above). For body copy, character or word reveals feel more natural.
The Text Reveal animation and Cinematic Text animation both use variations of this pattern.
Pattern 3: Word Stagger
Characters work for headlines. Lines work for big display text. Words work for body copy and subheadings where you want a more readable reveal.
useGSAP(() => {
if (!fontsLoaded) return;
const split = SplitText.create(paragraphRef.current, {
type: "words",
});
gsap.from(split.words, {
opacity: 0,
y: 20,
filter: "blur(4px)",
duration: 0.5,
stagger: 0.04,
ease: "power3.out",
scrollTrigger: {
trigger: paragraphRef.current,
start: "top 85%",
once: true,
},
});
return () => split.revert();
}, { scope: containerRef, dependencies: [fontsLoaded] });
filter: "blur(4px)"filterPattern 4: Scramble Text
A different kind of effect: the text starts scrambled and resolves to the real content. Works well for technical or data-driven contexts.
GSAP has a plugin for this called ScrambleText, but you can get 80% of the effect with SplitText and a custom approach:
useGSAP(() => {
if (!fontsLoaded) return;
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const originalText = headlineRef.current.textContent;
const split = SplitText.create(headlineRef.current, { type: "chars" });
split.chars.forEach((char, i) => {
const original = char.textContent;
let iterations = 0;
const interval = setInterval(() => {
char.textContent = chars[Math.floor(Math.random() * chars.length)];
if (iterations >= i * 3) {
char.textContent = original;
clearInterval(interval);
}
iterations++;
}, 50);
});
return () => split.revert();
}, { scope: containerRef, dependencies: [fontsLoaded] });
The delay is proportional to the character index. Earlier characters resolve first. This creates a left-to-right resolution that reads naturally.
The Text Scramble animation has a production-ready version of this with proper cleanup.
Pattern 5: Scroll-Scrubbed Text
Instead of triggering once on scroll, you tie the animation directly to scroll position. The text reveals as you scroll, pauses if you stop, reverses if you scroll back up.
useGSAP(() => {
if (!fontsLoaded) return;
const split = SplitText.create(headlineRef.current, { type: "words" });
gsap.from(split.words, {
opacity: 0.1,
duration: 1,
stagger: 0.1,
ease: "none",
scrollTrigger: {
trigger: headlineRef.current,
start: "top 70%",
end: "bottom 40%",
scrub: 1,
},
});
return () => split.revert();
}, { scope: containerRef, dependencies: [fontsLoaded] });
scrub: 1ease: "none"Performance Considerations
A few things that catch people out:
Avoid animating filter
split.revert()useGSAPDon't split too many elements at once. Splitting 10 paragraphs of body copy on page load is expensive. Use ScrollTrigger to split lazily: only split when the element is close to the viewport.
ScrollTrigger.create({
trigger: sectionRef.current,
start: "top 120%",
onEnter: () => {
const split = SplitText.create(sectionRef.current.querySelectorAll("p"), {
type: "lines",
});
// animate...
},
once: true,
});
observeChanges: trueconst split = SplitText.create(el, {
type: "lines",
observeChanges: true,
});
This is available in GSAP 3.13+.
Accessibility
Splitting text breaks screen reader context. A headline that reads "Hello World" becomes 10 separate elements. Screen readers will announce each character individually.
Fix this witharia-labelaria-hiddenconst el = headlineRef.current;
el.setAttribute("aria-label", el.textContent);
const split = SplitText.create(el, { type: "chars" });
split.chars.forEach((char) => char.setAttribute("aria-hidden", "true"));
prefers-reduced-motionconst prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!prefersReduced) {
const split = SplitText.create(el, { type: "chars" });
gsap.from(split.chars, { /* animation */ });
}
When to Use Which Type
| Split type | Best for | Avoid when |
|---|---|---|
chars | Headlines, short labels, hero text | Long paragraphs (too much DOM) |
words | Subheadings, medium-length copy | Very short text (awkward stagger) |
lines | Display text, large quotes | Body copy under 18px |
{ type: "chars,lines" }Common Mistakes
Not reverting SplitText. The most common one. In React especially, not reverting on component unmount causes stale DOM nodes and broken re-renders.
Animating before fonts load. If your custom font loads after you split, all the line breaks are wrong. Always wait fordocument.fonts.readystagger: 0.05Forgetting force3D: true
gsap.from(split.chars, {
y: 40,
opacity: 0,
force3D: true, // always
duration: 0.6,
stagger: 0.02,
ease: "expo.out",
});
Skip the Setup
If you want text animations without wiring all of this up yourself, Annnimate has a full text animation collection. Each one ships with React and HTML code. Production-ready, scroll-triggered, and accessible out of the box.
- Character Appear — character-by-character reveal on scroll
- Text Reveal — line mask reveal for headings
- Cinematic Text — dramatic large-format text entrance
- Folding Text — perspective-based 3D line fold
- Text Scramble — randomized character resolve
- Popping Text — bouncy word-by-word pop in
Copy, paste, customize with data attributes. No GSAP setup required.
Summary
Text animation with GSAP SplitText comes down to three things: split the right unit (chars, words, or lines), animate with stagger, and always revert on cleanup.
The patterns above cover 90% of what you'll need in production. Start with the line mask reveal for headlines. Add character reveals for shorter labels. Use scroll-scrub for editorial content.
If you want to skip building from scratch, the Annnimate text animations are the production-ready versions of everything in this post.
Written by
Julian Fella
Founder
Related reading

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.

GSAP ScrollTrigger Tutorial: Animate on Scroll (2025)
Learn GSAP ScrollTrigger with step-by-step examples. Includes code snippets, performance tips, and ready-to-use scroll animations for your project.

GSAP vs Framer Motion vs React Spring: Which Should You Use in 2026?
Comparing GSAP, Framer Motion, and React Spring for React animation. Bundle sizes, performance data, code examples, and honest recommendations.