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

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.

GSAP SplitText tutorial showing character and line text animations with scroll triggers

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>
or
<span>
. This gives GSAP something to animate independently.

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" });
That gives you
split.chars
,
split.words
, and
split.lines
as arrays. Pass them to any GSAP method.

Setup

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);
In React, register once at the module level and use the
useGSAP
hook:
import { 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] });
The
stagger: 0.02
is the key. 20ms between each character. At 20 characters that's a 400ms cascade. Fast enough to feel snappy, slow enough to actually read. For longer headlines, drop it to
0.01
. For short ones (3-4 words), go up to
0.04
.

You 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 setting
overflow: hidden
on each wrapper.
useGSAP(() => { 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: 110
pushes the text just past the bottom of its container. When it animates to 0, it slides into view. Because the container clips it, there's no visible starting position.

This 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] });
The
filter: "blur(4px)"
adds a subtle softness at the start. Animating
filter
has a small performance cost, so use it sparingly. On long paragraphs, skip it.

Pattern 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: 1
links the animation to scroll with a 1-second lag. The lag makes it feel physical rather than mechanical.
ease: "none"
on scrubbed animations is intentional. The scrollbar is the easing.

Performance Considerations

A few things that catch people out:

Avoid animating

filter
on mobile. Blur filters are GPU-accelerated on desktop but can cause frame drops on lower-end phones. Test on a mid-range Android before shipping.

Always call
split.revert()
on unmount.
SplitText wraps your text in dozens of elements. If you don't revert, those stay in the DOM. In React, return the revert from your
useGSAP
callback.

Don'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, });
Line-splitting is fragile on resize. When the window resizes, lines reflow and your split is wrong. Either re-split on resize (expensive) or use
observeChanges: true
in newer GSAP versions:
const 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 with
aria-label
on the container and
aria-hidden
on the split elements:
const 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"));
Also respect
prefers-reduced-motion
. Users who've asked for reduced motion shouldn't get character animations regardless.
const 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 typeBest forAvoid when
chars
Headlines, short labels, hero textLong paragraphs (too much DOM)
words
Subheadings, medium-length copyVery short text (awkward stagger)
lines
Display text, large quotesBody copy under 18px
You can combine types.
{ type: "chars,lines" }
gives you both arrays. Animate lines first, then chars inside each line for a compound effect.

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 for
document.fonts.ready
. Stagger too long. A 60-character headline with
stagger: 0.05
takes 3 full seconds. That's too long. Keep the total cascade under 1 second for most headlines.

Forgetting

force3D: true
. Add it to all GSAP animations. Without it, transforms aren't hardware accelerated on every browser.

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

Julian Fella

Founder

Related reading

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 ScrollTrigger tutorial showing scroll-based animation with trigger markers and 60fps performance
Tutorials·March 27, 2026

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.

Read article
Read article
GSAP vs Framer Motion vs React Spring comparison for React animation libraries in 2026
Comparisons·April 3, 2026

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.

Read article
Read article

On This Page

Stay ahead of the curve

Join 1000+ creators getting animation updates. Unsubscribe anytime.

Pages

  • Home
    Home
  • Animations
    Animations
  • Blog
    Blog
  • FAQ
    FAQ
  • Pricing
    Pricing
  • Platform
    Platform
  • Changelog
    Changelog
  • Roadmap
    Roadmap

Social

  • LinkedIn
    LinkedIn
  • Instagram
    Instagram
  • X / Twitter
    X / Twitter

Contact

  • Email me
    Email me

Legal

  • Privacy Policy
    Privacy Policy
  • Terms of Service
    Terms of Service
  • Cookie Policy
    Cookie Policy
  • Refund Policy
    Refund Policy

© 2026 Annnimate. All rights reserved.