Masked Text Reveal (GSAP SplitText)
Scroll-triggered text reveal where lines, words, or characters slide up from behind a hidden mask as the element enters the viewport. Includes a production-ready function with per-element split-type control and a simple basic version.
Setup — External Scripts
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script><script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script><script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/SplitText.min.js"></script>Code
<!-- Basic -->
<h1 data-split="heading">Your heading here</h1>
<!-- Advanced: control split type per element -->
<h1 data-split="heading" data-split-reveal="lines">Lines reveal</h1>
<h2 data-split="heading" data-split-reveal="words">Words reveal</h2>
<p data-split="heading" data-split-reveal="chars">Characters reveal</p>/* Hide headings until JS runs to prevent FOUC */
[data-split="heading"] {
visibility: hidden;
}gsap.registerPlugin(SplitText, ScrollTrigger);
const splitConfig = {
lines: { duration: 0.8, stagger: 0.08 },
words: { duration: 0.6, stagger: 0.06 },
chars: { duration: 0.4, stagger: 0.01 },
};
function initMaskTextScrollReveal() {
document.querySelectorAll('[data-split="heading"]').forEach(heading => {
gsap.set(heading, { autoAlpha: 1 });
const type = heading.dataset.splitReveal || 'lines';
const typesToSplit =
type === 'lines' ? ['lines'] :
type === 'words' ? ['lines', 'words'] :
['lines', 'words', 'chars'];
SplitText.create(heading, {
type: typesToSplit.join(', '),
mask: 'lines',
autoSplit: true,
linesClass: 'line',
wordsClass: 'word',
charsClass: 'letter',
onSplit(instance) {
const targets = instance[type];
const config = splitConfig[type];
return gsap.from(targets, {
yPercent: 110,
duration: config.duration,
stagger: config.stagger,
ease: 'expo.out',
scrollTrigger: {
trigger: heading,
start: 'clamp(top 80%)',
once: true,
},
});
},
});
});
}
document.addEventListener("DOMContentLoaded", () => {
document.fonts.ready.then(() => {
initMaskTextScrollReveal();
});
});Notes
- •Only the minimum split depth needed is requested — `lines` only for line reveals, `lines + words` for word reveals — to keep the DOM lean.
- •`clamp(top 80%)` as the ScrollTrigger start value prevents the animation from firing mid-scroll if the element is already past the trigger point on page load.
- •`autoSplit: true` re-splits on resize and `onSplit` returning the tween lets GSAP clean up the old animation automatically before creating a new one.
- •SplitText v13+ automatically adds ARIA attributes to maintain screen-reader accessibility without any extra work.
Guide
Basic version
Just add `data-split="heading"` to any text element. Lines will slide up from a masked overflow on scroll. The `onSplit` callback returns the tween so GSAP can kill it before re-splitting on resize.
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('[data-split="heading"]').forEach(heading => {
SplitText.create(heading, {
type: "lines",
autoSplit: true,
mask: "lines",
onSplit(instance) {
return gsap.from(instance.lines, {
duration: 0.8,
yPercent: 110,
stagger: 0.1,
ease: "expo.out",
scrollTrigger: { trigger: heading, start: "top 80%", once: true }
});
}
});
});
});Split type per element
Add `data-split-reveal` with a value of `lines`, `words`, or `chars` to control how each element is split. The `splitConfig` object at the top of the script defines the duration and stagger for each type.
Preventing FOUC
Hide elements with `visibility: hidden` in CSS and reveal them with `gsap.set(heading, { autoAlpha: 1 })` right before animating. Wrap initialisation in `document.fonts.ready` so splits happen after custom fonts are loaded.