Highlight Text on Scroll

Characters in a heading fade from a low opacity to fully visible as the user scrolls, creating a smooth word-by-word highlight effect driven entirely by scroll position. No pseudo-elements or duplicate DOM nodes required.

GSAPScrollTriggerSplitTextScrollTypography

Setup — External Scripts

GSAP CDN
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
ScrollTrigger CDN
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script>
SplitText CDN
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/SplitText.min.js"></script>

Code

index.html
html
<h1 data-highlight-text>Add your heading here</h1>
script.js
javascript
gsap.registerPlugin(ScrollTrigger, SplitText)

function initHighlightText() {
  document.querySelectorAll("[data-highlight-text]").forEach(heading => {
    const scrollStart  = heading.getAttribute("data-highlight-scroll-start") || "top 90%"
    const scrollEnd    = heading.getAttribute("data-highlight-scroll-end")   || "center 40%"
    const fadedValue   = heading.getAttribute("data-highlight-fade")         || 0.2
    const staggerValue = heading.getAttribute("data-highlight-stagger")      || 0.1

    new SplitText(heading, {
      type: "words, chars",
      autoSplit: true,
      onSplit(self) {
        let ctx = gsap.context(() => {
          let tl = gsap.timeline({
            scrollTrigger: {
              scrub: true,
              trigger: heading,
              start: scrollStart,
              end: scrollEnd,
            }
          })
          tl.from(self.chars, {
            autoAlpha: fadedValue,
            stagger: staggerValue,
            ease: "linear"
          })
        });
        return ctx;
      }
    });
  });
}

document.addEventListener("DOMContentLoaded", () => {
  initHighlightText();
});

Notes

  • `data-highlight-fade` sets the starting opacity of un-highlighted characters (default 0.2). `data-highlight-stagger` controls the offset between characters — lower values create a smoother wave, higher values highlight one letter at a time.
  • `autoSplit: true` re-splits the text on resize so line wrapping is always correct; the returned GSAP context kills the old ScrollTrigger before a new one is created.
  • Because the animation uses `scrub: true`, the highlight follows scroll position directly — scrolling back up un-highlights the text.
  • No pseudo-elements, duplicated text, or clip masks are needed — just a straight opacity tween on each character span.

Guide

Customisation attributes

All attributes are optional and fall back to sensible defaults. Add them to the same element as `data-highlight-text`.

<h1
  data-highlight-text
  data-highlight-scroll-start="top 90%"
  data-highlight-scroll-end="center 40%"
  data-highlight-fade="0.2"
  data-highlight-stagger="0.1"
>
  Your heading here
</h1>