AnimationsSimpleApril 9, 2026
Highlight Text
A polymorphic text wrapper that uses GSAP SplitText and ScrollTrigger scrub to fade each character in as you scroll. Works on any heading or paragraph element via the `as` prop.
View Full Demo →Preview
Source
demo.jsx
import HighlightText from "./index.jsx";
import styles from "./demo.module.css";
export default function HighlightTextDemo() {
return (
<div className={styles.page}>
{/* Hero — scroll down to see the effect */}
<section className={styles.hero}>
<p className={styles.heroLabel}>Scroll to reveal</p>
<h1 className={styles.heroTitle}>
Highlight<br />Text
</h1>
</section>
{/* Highlight section */}
<section className={styles.section}>
<p className={styles.sectionLabel}>Manifesto</p>
<HighlightText
as="h2"
scrollStart="top 85%"
scrollEnd="center 35%"
fade={0.15}
stagger={0.08}
className={styles.heading}
>
We build websites that move people — not just pixels.
</HighlightText>
<HighlightText
as="p"
scrollStart="top 90%"
scrollEnd="center 40%"
fade={0.2}
stagger={0.05}
className={styles.body}
>
Every project starts with an animation language. A set of principles that
define how your brand communicates through motion, timing, and space.
</HighlightText>
</section>
<div className={styles.spacer} />
</div>
);
}
index.jsx
"use client";
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
import { SplitText } from "gsap/SplitText";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(SplitText, ScrollTrigger);
export default function HighlightText({
as: Tag = "h2",
children,
scrollStart = "top 90%",
scrollEnd = "center 40%",
fade = 0.2,
stagger = 0.1,
className = "",
}) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
let ctx;
const split = new SplitText(el, {
type: "words, chars",
autoSplit: true,
onSplit(self) {
ctx = gsap.context(() => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: el,
start: scrollStart,
end: scrollEnd,
scrub: true,
},
});
tl.from(self.chars, {
autoAlpha: fade,
stagger,
ease: "linear",
});
});
return ctx;
},
});
return () => {
ctx?.revert();
split?.revert();
};
}, [scrollStart, scrollEnd, fade, stagger]);
return (
<Tag ref={ref} className={className}>
{children}
</Tag>
);
}
demo.module.css
.page {
background: #0d0d0d;
color: #efeeec;
}
/* Hero — pushes highlight section below the fold */
.hero {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 3rem 4vw;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.heroLabel {
font-size: 0.75rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.35);
margin: 0 0 1.5rem;
}
.heroTitle {
font-size: clamp(3.5rem, 10vw, 9rem);
font-weight: 500;
letter-spacing: -0.03em;
line-height: 0.95;
margin: 0;
}
/* Highlight section */
.section {
padding: 15vh 4vw;
}
.sectionLabel {
font-size: 0.75rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.3);
margin: 0 0 3rem;
}
.heading {
font-size: clamp(2.5rem, 6vw, 6rem);
font-weight: 500;
letter-spacing: -0.03em;
line-height: 1.05;
margin: 0 0 6rem;
max-width: 16em;
}
.body {
font-size: clamp(1.25rem, 2.5vw, 2rem);
font-weight: 400;
line-height: 1.5;
max-width: 28em;
margin: 0;
}
.spacer {
height: 30vh;
}
Dependencies
gsap