text·free
Split Text
Headline that animates in by characters, words, or masked lines.
Motion that ships.
Masked line reveals for paragraphs, powered by GSAP SplitText. Resizes are handled for you.
Installation
terminal
pnpm add gsap @gsap/reactThen copy the component below into src/components/motion/split-text.tsx. It expects @/lib/gsap (plugin registration) and @/lib/utils (cn).
Source
src/components/motion/split-text.tsx
"use client";
import { useRef, type ReactNode } from "react";
import { gsap, SplitText as GsapSplitText, useGSAP } from "@/lib/gsap";
import { cn } from "@/lib/utils";
export interface SplitTextProps {
children: ReactNode;
as?: "h1" | "h2" | "h3" | "h4" | "p" | "span" | "div";
className?: string;
/** Split unit. */
type?: "chars" | "words" | "lines";
duration?: number;
stagger?: number;
delay?: number;
/** Animate on scroll into view instead of on mount. */
onScroll?: boolean;
y?: number;
ease?: string;
}
export function SplitText({
children,
as = "h2",
className,
type = "words",
duration = 0.8,
stagger = 0.04,
delay = 0,
onScroll = true,
y = 60,
ease = "power4.out",
}: SplitTextProps) {
const ref = useRef<HTMLDivElement>(null);
// All allowed tags share the HTMLElement API we use; cast for JSX typing.
const Tag = as as "div";
useGSAP(
() => {
const el = ref.current;
if (!el) return;
const split = GsapSplitText.create(el, {
type: type === "lines" ? "lines" : `${type},lines`,
linesClass: "split-line",
mask: type === "lines" ? "lines" : undefined,
});
const targets =
type === "chars" ? split.chars : type === "words" ? split.words : split.lines;
gsap.from(targets, {
yPercent: type === "lines" ? 100 : 0,
y: type === "lines" ? 0 : y,
opacity: 0,
duration,
stagger,
delay,
ease,
scrollTrigger: onScroll ? { trigger: el, start: "top 85%" } : undefined,
});
return () => split.revert();
},
{ scope: ref, dependencies: [type, duration, stagger, delay, onScroll, y, ease] }
);
return (
<Tag ref={ref} className={cn("will-change-transform", className)}>
{children}
</Tag>
);
}