webdevpuneet.com

Scroll Letter Stagger — Free GSAP Split Text Snippet

Scroll Letter Stagger · Scroll · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<section class="sls-top"><p>Scroll ↓</p></section>
<section class="sls-stage" id="slsStage">
  <h2 class="sls-line" id="slsLine1" data-text="Letters that"></h2>
  <h2 class="sls-line sls-grad" id="slsLine2" data-text="ride the scroll"></h2>
  <div class="sls-rule" id="slsRule"></div>
</section>
<section class="sls-bottom"><p>Scroll up — every letter retreats in reverse order.</p></section>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Plugin-free split
Twelve lines replace SplitText for this case.
Masked rise
overflow: hidden lines hide waiting letters.
Self-scaling offset
yPercent tracks the responsive font size.
Settling rotation
Letters un-rotate as they land.
Scrubbed wave
Stagger order becomes spatial order.
Overlapped lines
Line two starts before line one finishes.
Drawn rule
A gradient underline signs off the headline.
Split-safe gradient
background-clip applied per character.

About this UI Snippet

Scroll Letter Stagger — Split-Text Headlines Scrubbed by Scroll

Screenshot of the Scroll Letter Stagger snippet rendered live

The scroll letter stagger is the split-text entrance — each character rising from below its line, straightening as it lands — but scrubbed to the scrollbar instead of played on a timer: letters ride up exactly as far as you've scrolled, and retreat in reverse order when you scroll back. This snippet builds it with GSAP ScrollTrigger (from a CDN), a hand-rolled character splitter, and masked line boxes — no SplitText plugin.

A twelve-line splitter instead of a plugin

Each headline stores its copy in data-text; a small function converts it into inline-block <span class="ch"> elements, mapping spaces to \u00A0 so they keep width. Inline-block is the load-bearing detail — plain inline spans ignore transforms, so every character must become a box before yPercent or rotation can touch it. Reading from an attribute (not innerHTML) also makes the split idempotent: re-running it can't nest spans inside spans.

yPercent + overflow: hidden = the masked rise

Characters start at yPercent: 120 — 120% of *their own height* below their resting spot — while each line has overflow: hidden. The line box acts as a mask: letters are genuinely invisible until they cross their baseline, rising as if from behind a sill. Using yPercent instead of a pixel offset means the hide distance self-scales with the clamped responsive font size, and a small padding-bottom on the line keeps descenders (y, g) from being clipped at rest.

Rotation gives the rise its physicality

Each character also un-rotates from 9° (line one) or −9° (line two) as it lands. Straight vertical rises read as blinds; adding a settling rotation makes letters feel individually *placed*, and mirroring the angle between lines makes the two lines visually answer each other.

Stagger order becomes spatial order under scrub

stagger: 0.05 offsets each character's tween along the timeline, and because the timeline is scrubbed, temporal offset becomes spatial: at any scroll position the left letters are further through their arc than the right ones, producing a wave that sweeps across the word. Scrolling up runs the wave backward — last letter retreats first — for free.

Three beats on one timeline

Line one starts at 0, line two overlaps at 0.35 (starting while line one's tail is still landing, so the headline reads as one gesture), and the gradient rule draws from 0 width at 1.1 — a sign-off that only completes once the type has fully landed. The rule animates width deliberately: it's a single tiny element, so the layout cost is nil and it avoids a scale-x's stretched-gradient artifact.

Gradient text survives the split

The second line's gradient uses background-clip: text applied to the *characters*, so each span carries its slice of the gradient. Applying it to the parent wouldn't survive the split, since transformed inline-blocks establish their own paint containers.

Customizing it

Edit the data-text attributes (the splitter adapts), flip the wave direction with stagger: { each: 0.05, from: 'end' }, or add a third line at a later position. Related type effects: word-level text reveal on scroll, the timer-based split text, scrubbed typing in scroll typewriter, and self-drawing outlines in scroll text draw.

Step by step

How to Use

  1. 1
    Add the GSAP CDNsInclude gsap and ScrollTrigger from the CDN panel.
  2. 2
    Paste HTML, CSS, and JSHeadlines split into character spans on load.
  3. 3
    Scroll into the stageLetters rise from behind their line masks.
  4. 4
    Watch the waveThe stagger sweeps left to right across each line.
  5. 5
    Scroll back upLetters retreat in reverse order.
  6. 6
    Change the copyEdit data-text; the splitter and wave adapt.

Real-world uses

Common Use Cases

Hero headline entrances
Land a statement letter by letter as the reader scrolls, and exit it later with a scroll hero exit.
Chapter title punctuation
Introduce chapters in a scroll pin story, with masked lines hiding waiting letters until they rise.
Portfolio name reveals
Land your name letter by letter, then follow with a scroll reveal grid of projects as the portfolio opens up.
Campaign statements
Pair the type with a scroll word wheel, or brighten whole paragraphs with text reveal scroll.
Plugin-free split learning
See how a dozen lines of JavaScript replace a split-text plugin here, with yPercent scaling to the responsive font size, and compare with the timer-based split text.
Related: Scroll Milestone Confetti
See the Scroll Milestone Confetti for a related scroll pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Each line keeps its copy in a data-text attribute; a small loop creates one inline-block span per character, mapping spaces to non-breaking spaces so they keep width. Inline-block matters because plain inline elements ignore transforms. Reading from the attribute also makes the split idempotent — it can never nest spans on a re-run.

Each character starts at yPercent 120 — 120% of its own height below its resting position — while the line box clips with overflow: hidden. The line acts as a mask, so letters are genuinely hidden until they cross the baseline. yPercent self-scales with the clamp()ed font size, and the line's small bottom padding keeps descenders from being clipped at rest.

stagger: 0.05 offsets each character's tween along the timeline, and scrubbing converts that temporal offset into a spatial one: at any scroll position, earlier letters are further through their rise than later ones. Reversing scroll runs the wave backward automatically. from: 'end' or from: 'center' in a stagger object changes the sweep direction.

background-clip: text paints the element's background through its glyphs, but transformed inline-block spans establish their own paint containers — a parent-level gradient wouldn't show through them. Giving every span the gradient keeps each character carrying its slice, so the line reads as one continuous ramp even mid-animation.

Run the split and timeline in a mount effect — useEffect, onMounted, or ngAfterViewInit — against refs, guarding against double-splitting (StrictMode re-runs) by checking for existing .ch children, and revert a gsap.context in the cleanup so the pin unregisters. Alternatively pre-split in JSX/template by mapping the string to spans, then only the timeline lives in the effect. Line styles port to Tailwind cleanly.