VeloxUI
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/react

Then 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>
  );
}