shipping/JOG — 03:41 GMT+7/48.3 °C CPU/v 07.0 · build 2026.05.12
lat −7.7956 · lon 110.3695/ntwk · online/open for projects
back to notesnotes2026gsap-splittext-kinetic-type
[ essay no. 05 ]craftmay 13, 20263 min612 wordsrevision 1live

Kinetic Type

Split text into characters, words, or lines and animate each piece independently. The technique behind every scroll-reveal and cinematic title on award-winning sites.

d
devrangga hazza mahiswaracreative engineer · jogja, id
share on 𝕏

creative.

click to advance · loops automatically
01

Why SplitText

Plain CSS animations move the whole text block as one unit. SplitText wraps every character (or word, or line) in its own <span>, so you can stagger them, reverse them, or drive each one from scroll position independently.

Until GSAP 3.12, SplitText was a paid Club GSAP plugin. It's now free in the main gsap package.

02

Register the plugin

SplitText must be registered once before any usage. Do it at module scope in any client component.

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

In Next.js, keep all GSAP code inside "use client" components — the plugins access document, which doesn't exist during server rendering.

03

Split and animate

new SplitText(el, { type: "chars" }) wraps each character in a <span>. The instance gives you .chars, .words, and .lines arrays to target with GSAP.

"use client";
 
import { useEffect, useRef } from "react";
import gsap from "gsap";
import { SplitText } from "gsap/SplitText";
 
gsap.registerPlugin(SplitText);
 
export function RevealHeading({ children }: { children: string }) {
  const ref = useRef<HTMLHeadingElement>(null);
 
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
 
    const split = new SplitText(el, { type: "chars" });
 
    gsap.fromTo(
      split.chars,
      { opacity: 0, y: 40, rotateX: -90, transformOrigin: "50% 0%" },
      {
        opacity: 1,
        y: 0,
        rotateX: 0,
        stagger: 0.04,
        duration: 0.55,
        ease: "back.out(1.8)",
      },
    );
 
    return () => split.revert();
  }, [children]);
 
  return (
    <h1 ref={ref} style={{ perspective: "600px" }}>
      {children}
    </h1>
  );
}

perspective: "600px" on the parent is required for rotateX to look 3D rather than flat.

04

Wire it to scroll

Pair with ScrollTrigger so the reveal fires when the heading enters the viewport rather than on mount.

useEffect(() => {
  const el = ref.current;
  if (!el) return;
 
  const split = new SplitText(el, { type: "chars" });
 
  gsap.fromTo(
    split.chars,
    { opacity: 0, y: 32 },
    {
      opacity: 1,
      y: 0,
      stagger: 0.035,
      duration: 0.5,
      ease: "power3.out",
      scrollTrigger: {
        trigger: el,
        start: "top 85%",
        once: true,
      },
    },
  );
 
  return () => split.revert();
}, []);

once: true is important for headings — you don't want a title to disappear and reappear as the user scrolls back up.

05

Word and line splits

type: "chars" is the most granular. "words" groups by word, "lines" groups by the rendered line breaks. You can combine them:

const split = new SplitText(el, { type: "chars words lines" });
// split.chars → every character
// split.words → every word
// split.lines → every line

A common pattern: stagger words in, then stagger chars within each word for a layered feel.

gsap.timeline()
  .from(split.lines, {
    opacity: 0,
    y: 20,
    stagger: 0.1,
    duration: 0.6,
    ease: "power2.out",
  })
  .from(
    split.chars,
    { opacity: 0, stagger: 0.008, duration: 0.4 },
    "<0.1",
  );
06

Scramble text effect

GSAP's ScrambleTextPlugin pairs naturally with SplitText for the "hacker text" reveal — characters randomize before settling on the final value.

import { ScrambleTextPlugin } from "gsap/ScrambleTextPlugin";
 
gsap.registerPlugin(ScrambleTextPlugin);
 
gsap.to(split.words, {
  duration: 0.8,
  scrambleText: {
    text: "{original}",
    chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
    speed: 0.5,
  },
  stagger: 0.05,
  ease: "none",
});
07

Always revert on cleanup

SplitText injects spans directly into the DOM. If the component unmounts without reverting, orphaned spans can break hydration on the next render.

useEffect(() => {
  const split = new SplitText(ref.current, { type: "chars" });
  // ... animations ...
 
  return () => {
    split.revert(); // removes injected spans, restores original text
  };
}, []);

In React Strict Mode, effects run twice. split.revert() in the cleanup prevents double-split, which would produce nested <span><span>c</span></span> per character.

— end of essay · published may 13, 2026 · 612 words · 3 min
[ if this moved you ]

keep reading.

three essays in the same key · pick one