✳ Nairobi, Kenya
- WhatsApp+254 799 756 331
- Emailhello@muvevi.com
- LinkedInlinkedin.com/in/muvevi
- GitHubgithub.com/mutua-muvevi
- Twitter / X@muvevi
Available for freelance & full-time roles.
Let's build something great.
GSAP text animations with SplitType
— the definitive guide
Split text animations are everywhere. Most implementations have subtle bugs — wrong stagger direction, no SSR guard, missing cleanup. Here's the version that holds up.
SplitType + GSAP is the standard stack for cinematic text reveals. The animation is simple; the edge cases are not.
The SSR crash. SplitType splits the DOM on init. In Next.js App Router with server components, the DOM doesn't exist at render time. If you initialise SplitType outside a useEffect, you'll get a crash. The fix: always initialise inside useEffect(() => { const split = new SplitType(ref.current) ... }, []) and return a cleanup that calls split.revert().
Chars vs words vs lines. types: "chars" is for heading reveals where each letter animates independently. types: "lines" is for paragraphs — it wraps each visual line in a span, which means the split changes on window resize. types: "words" is rarely what you want by itself; combine it with chars for words-then-letters animations.
useEffect(() => {
if (!headingRef.current) return;
const split = new SplitType(headingRef.current, {
types: "chars",
tagName: "span",
});
const tl = gsap.timeline();
tl.fromTo(
split.chars,
{ yPercent: 120, rotateX: -40, opacity: 0 },
{
yPercent: 0, rotateX: 0, opacity: 1,
duration: 1.1, ease: "power4.out",
stagger: 0.025,
}
);
return () => {
tl.kill();
split.revert();
};
}, []);The perspective wrapper. The rotateX transform looks flat without CSS perspective on the parent. Add style={{ perspective: "800px" }} to the wrapper element. Without it, rotateX animations appear to scale rather than rotate in 3D.
Resize handling for line splits. When you split by lines, the line boundaries are computed at render time. If the user resizes the window, the visual lines change but the spans don't update. The fix: add a ResizeObserver (or GSAP's MatchMedia for breakpoint-based re-splits) that calls split.split() on width changes. Debounce at 200ms.
ScrollTrigger with SplitType. If the text is below the fold, the split happens before the text is in view but the animation fires immediately. Guard with scrollTrigger: { trigger: ref.current, start: "top 85%" } so the animation waits until the element enters the viewport. Combined with stagger, this gives you the "text cascades in as you scroll" effect that's everywhere on award-winning sites.