Motion

GSAP text animation: 6 SplitText effects with code

Line reveals, word staggers, a char wave, scramble, scroll scrub and a hover roll. Each one is short, works in React and stays readable for screen readers.

By Nabeel Mustafa··7 min read

Cover image for GSAP text animation: 6 SplitText effects with code

A GSAP text animation usually starts with SplitText, which splits a heading into lines, words or characters so GSAP can animate each piece with a stagger. Since GSAP 3.13 SplitText is free, smaller, and has built-in masks, autoSplit for resizes and font loads, and screen reader handling. Below are six effects I use, each with code you can paste into a React or Next.js project.

Every example uses the same setup: one file registers the plugins, and components animate inside useGSAP so everything is cleaned up on unmount. This is the file on this site.

lib/gsap.ts
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
import { useGSAP } from "@gsap/react";

gsap.registerPlugin(ScrollTrigger, SplitText, useGSAP);
gsap.defaults({ ease: "power3.out", duration: 1 });

export const prefersReducedMotion = () =>
  typeof window !== "undefined" &&
  window.matchMedia("(prefers-reduced-motion: reduce)").matches;

export { gsap, ScrollTrigger, SplitText, useGSAP };

What changed in SplitText in GSAP 3.13

The old SplitText was a paid Club plugin. The 3.13 rewrite is free for everyone and smaller. The changes that matter in daily work:

  • `mask`: set mask: "lines" (or words, chars) and each piece gets a wrapper with clipped overflow. No extra markup or CSS for reveal effects.
  • `autoSplit`: splits again when fonts finish loading or the element width changes, so lines stay correct.
  • `onSplit`: a callback that runs after every split. Return your animation from it and SplitText keeps its progress across re-splits and cleans it up.
  • `aria`: the default "auto" puts an aria-label with the full text on the element and hides the split pieces from screen readers.
  • `SplitText.create()`: the new way to make an instance. revert() puts the original HTML back.

How to build a GSAP split text animation in React

Create the split inside useGSAP, scope it to a ref, and revert it in the cleanup. The masked line reveal below is the SplitHeading component that runs every section title on this site. I use fromTo() instead of from(), because in React an effect can run twice, and from() reads the current state, which may be the middle of the last animation.

components/motion/SplitHeading.tsx
"use client";

import { useRef } from "react";
import { gsap, useGSAP, SplitText, prefersReducedMotion } from "@/lib/gsap";

export default function SplitHeading({ children }: { children: React.ReactNode }) {
  const ref = useRef<HTMLHeadingElement>(null);

  useGSAP(
    () => {
      if (prefersReducedMotion() || !ref.current) return;
      const split = SplitText.create(ref.current, {
        type: "lines",
        mask: "lines",
        autoSplit: true,
        onSplit: (self) =>
          gsap.fromTo(
            self.lines,
            { yPercent: 110 },
            {
              yPercent: 0,
              duration: 1.2,
              stagger: 0.09,
              ease: "power4.out",
              scrollTrigger: { trigger: ref.current, start: "top 85%", once: true },
            },
          ),
      });
      return () => split.revert();
    },
    { scope: ref },
  );

  return <h2 ref={ref}>{children}</h2>;
}

Six GSAP text animation effects

Each effect below is the body of a useGSAP callback. The component around it is the same as above.

1. Line reveal with a mask

This is the one above. Lines slide up from below a clipped edge. It is calm, it reads well for long headings, and it is my default for section titles. Keep the stagger under 0.1 seconds per line so a four line heading finishes in good time.

2. Word stagger

Words rise and fade in one after another. It is softer than a line reveal and works well for a hero subtitle. Masking by word gives a clean edge without a fade, if you prefer that.

ts
const split = SplitText.create(ref.current, { type: "words", mask: "words" });
gsap.fromTo(
  split.words,
  { yPercent: 100, autoAlpha: 0 },
  { yPercent: 0, autoAlpha: 1, duration: 0.8, stagger: 0.04, ease: "power3.out" },
);
return () => split.revert();

3. Character wave

Characters bob up and down in a loop, with the stagger offset making a wave. Each character gets its own repeat inside the stagger object, so the wave keeps its shape forever. Use it for a short word, like a logo or a 404 page, not for body text.

ts
const split = SplitText.create(ref.current, { type: "chars" });
gsap.to(split.chars, {
  y: -12,
  duration: 0.5,
  ease: "sine.inOut",
  stagger: { each: 0.05, repeat: -1, yoyo: true },
});
return () => split.revert();

4. Scramble text

This one does not need SplitText. ScrambleTextPlugin ships in the same gsap package (also free since 3.13) and swaps characters with random ones before it settles on the real text. Put an aria-label with the final text on the element, because a screen reader may read the text while it is scrambled.

ts
import { ScrambleTextPlugin } from "gsap/ScrambleTextPlugin";
gsap.registerPlugin(ScrambleTextPlugin);

// inside useGSAP
const el = ref.current!;
gsap.to(el, {
  duration: 1.2,
  scrambleText: {
    text: el.textContent ?? "",
    chars: "upperCase",
    revealDelay: 0.3,
    speed: 0.4,
  },
});

5. Scroll-scrubbed reveal

Words go from faint to full as you scroll, tied to the scrollbar with scrub. It works for a statement paragraph that you want people to read slowly. Do not mask here; the text is already in place, only the opacity changes.

ts
const split = SplitText.create(ref.current, { type: "words" });
gsap.fromTo(
  split.words,
  { opacity: 0.15 },
  {
    opacity: 1,
    ease: "none",
    stagger: 0.1,
    scrollTrigger: { trigger: ref.current, start: "top 80%", end: "bottom 45%", scrub: true },
  },
);
return () => split.revert();

6. Hover character roll

On hover, each character rolls up and a copy rolls in from below. The copy is a text-shadow placed exactly one line lower, and the char mask hides it until the roll. Handlers that animate need contextSafe, so the tweens are part of the context and get cleaned up. Put the roll class on the element you split.

tsx
useGSAP(
  (_, contextSafe) => {
    const el = ref.current!;
    const split = SplitText.create(el, { type: "chars", mask: "chars", charsClass: "char" });
    const roll = contextSafe!(() =>
      gsap.fromTo(split.chars, { yPercent: 0 }, { yPercent: -100, duration: 0.4, stagger: 0.02, ease: "power2.inOut", overwrite: true }),
    );
    el.addEventListener("pointerenter", roll);
    return () => {
      el.removeEventListener("pointerenter", roll);
      split.revert();
    };
  },
  { scope: ref },
);
css
.roll {
  line-height: 1;
}
.roll .char {
  /* a copy of each char, one line below */
  text-shadow: 0 1em currentColor;
}

How to wait for fonts before splitting text

Lines depend on the font. If SplitText measures with the fallback font and the web font arrives a moment later, the line breaks are wrong. You have two fixes. The simple one is autoSplit: true with the animation in onSplit, as above: SplitText splits again when fonts load. The other is to wait yourself.

ts
document.fonts.ready.then(() => {
  const split = SplitText.create(ref.current, { type: "lines", mask: "lines" });
  // animate split.lines here
});

If you wait yourself, hide the heading in CSS with visibility: hidden and reveal it with autoAlpha in the animation. Otherwise the text shows, then jumps into its start position. With next/font, fonts are preloaded, so the wait is usually short.

Is SplitText accessible for screen readers?

By default, yes for plain text. With aria: "auto", SplitText adds an aria-label with the full text to the element and aria-hidden to the pieces, so a screen reader reads one sentence instead of letters. If the text has links or other nested markup, the label would flatten them. In that case set aria: "hidden" and add a visually hidden copy of the original markup next to it.

Motion is the second half of accessibility. Every effect above starts with a reduced motion check. For people who ask the OS for less motion, show the finished text with no movement. My guide on reduced motion with GSAP matchMedia covers the full pattern, and motion as information covers when text should move at all.

  • Do not split long body copy into chars. It creates hundreds of elements and slows the page.
  • Do not animate text that people need to read right now, like an error message.
  • Keep the real tag (h1, h2) on the element you split.

Wrapping up

SplitText is now free, small and safe to use in React when you follow three rules: create it in useGSAP, return the animation from onSplit with autoSplit on, and revert on cleanup. Pick one text effect per page and use it the same way everywhere, so motion feels like part of the design. If you want text motion like this built into your product site, see how I work on motion.

Frequently asked questions

Is GSAP SplitText free?

Yes. Since GSAP 3.13 every GSAP plugin is free, SplitText included, and that covers commercial projects too. You install the normal gsap package from npm and import it from gsap/SplitText. There is no separate club package or license key any more.

Does SplitText hurt SEO?

No. The text stays in the server-rendered HTML, so crawlers read it as normal. SplitText only wraps it in extra elements in the browser after the page loads. Keep the real heading tag (h1, h2) on the element you split, and the page structure stays the same for search engines.

Why does my split text break on resize?

Lines are measured once, at split time. When the container width changes, words move to other lines but your line elements stay the same. Set autoSplit: true and create the animation inside onSplit, returning it. SplitText then splits again on resize and font load and keeps the animation in sync.

Can I use SplitText with Framer Motion or CSS animations?

Yes. SplitText only splits the DOM; it does not care what animates the pieces. You can split, then add CSS classes with transition delays or animate the elements with another library. GSAP is the easiest match because it handles staggers, masks and cleanup in one place.

Need this built?Motion & InteractionScroll experiences, micro-interactions and launch films that make a product feel alive.