All components

Phive Text Scroll Animation

GSAP animation component · Published 2026-07-27 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

Scroll-driven sequence of pinned full-screen sections where giant uppercase words stretch open vertically (scaleY driven by each ScrollTrigger's scrub progress) and collapse as they leave; the final word scales up 10x while its dark backdrop fades to reveal a background image, then a SplitText headline reveals word-by-word.

How it's built

Categorytext
Techgsap, lenis
GSAP pluginsScrollTrigger, SplitText
Complexitypage
Performance costmedium
Mobile-safeyes
Scrollhijacks scrolling

scroll pin scrub scaley text-reveal typography lenis splittext

Rebuild it with AI

To reproduce this animation in your own project, copy the prompt below into Claude Code, Cursor or any AI coding agent. The prompt is validated — it describes the exact structure, timing and easing, so the agent rebuilds the effect faithfully and you can then adapt colors, copy and layout to your design.

The full prompt

Giant Stretching Text Scroll Animation

Goal

Build a scroll-driven typographic page: a sequence of full-screen pinned sections where a single giant uppercase word vertically stretches open (scaleY from 0 to the exact scale that fills the viewport) as the section scrolls into view, then collapses back to 0 while pinned. The final section instead blows the whole word block up 10x, cross-fades its dark backdrop away to reveal a full-bleed background photo, and finishes with a headline that reveals word-by-word via SplitText.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) with the GSAP plugins ScrollTrigger and SplitText, plus lenis for smooth scrolling. No build framework needed beyond a Vite-style dev server that resolves npm imports.

Layout / HTML

Five stacked <section> elements, each 100vw x 100svh:

  1. <section class="hero"><h1>This space intentionally loud</h1>
  2. <section class="sticky-text-1"><div class="text-container"><h1>Overdrive</h1></div>
  3. <section class="sticky-text-2"><div class="text-container"><h1>Static</h1></div>
  4. <section class="sticky-text-3"> — three children, in this order:
  5. <div class="bg-img"><img src="..." alt=""></div> (the background photo)
  6. <div class="text-container"><h1>Friction</h1></div>
  7. <div class="header"><h1>Overdrive always breaks the system</h1></div>
  8. <section class="outro"><h1>End of transmission</h1>

Load the script with <script type="module" src="./script.js">.

Styling

  • Palette as CSS custom properties on :root: --dark: rgba(17, 39, 11, 1) (near-black green) and --light: rgba(162, 255, 91, 1) (acid lime green).
  • Font: "Roboto Condensed" (Google Fonts, full variable weight range), applied to body. Global reset (* { margin:0; padding:0; box-sizing:border-box }).
  • All h1: text-transform: uppercase; font-size: 5rem; font-weight: 900; letter-spacing: -0.02em; line-height: 0.85; text-align: center;.
  • img { width:100%; height:100%; object-fit: cover; }.
  • Every section: position: relative; width: 100vw; height: 100svh; overflow: hidden;.
  • .hero and .outro: flex-centered, background var(--dark), color var(--light), their h1 at width: 50%.
  • .sticky-text-1 and .sticky-text-2: background var(--light), color var(--dark).
  • .sticky-text-3: color var(--light); its .text-container additionally gets background-color: var(--dark) (this dark plate is what fades out later).
  • .text-container and .bg-img: position: absolute; inset: 0 (top:0; left:0); width:100%; height:100%; will-change: opacity, transform; z-index: 1;.
  • .text-container h1: position: relative; left: -0.035em; letter-spacing: -0.05em; transform-origin: 50% 0%; transform: scaleY(0); — critical: the words start vertically collapsed and stretch from the TOP edge.
  • Per-section word sizes: .sticky-text-1 h1 = 23vw, font-weight: 300; .sticky-text-2 h1 = 35vw (weight 900); .sticky-text-3 .text-container h1 = 27vw, weight 900. Add will-change: transform on the first two.
  • .header: position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 50%; z-index: 2; (sits above the text-container).
  • Media query max-width: 1000px: base h1 drops to 3rem; .hero h1, .outro h1 and .header get width: calc(100% - 4rem).

GSAP effect (exhaustive)

Wrap everything in DOMContentLoaded. Register ScrollTrigger and SplitText.

Lenis smooth scroll
const lenis = new Lenis();
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
SplitText setup

Split the .header h1 with SplitText.create(header, { type: "words", wordsClass: "spotlight-word" }) and immediately gsap.set(split.words, { opacity: 0 }) so the headline starts invisible.

Dynamic target scale

For each of the three sticky sections compute targetScales[i] = section.offsetHeight / h1.offsetHeight (viewport height divided by the rendered text height). Recompute on window.resize. This is the scaleY value at which the stretched word exactly fills the section top-to-bottom. Scaling is applied by writing the inline style directly: element.style.transform = "scaleY(" + scale + ")" inside ScrollTrigger onUpdate callbacks (no tweens — everything is progress-mapped).

Sections 1 and 2 — stretch in, collapse while pinned

Each of .sticky-text-1 and .sticky-text-2 gets TWO ScrollTriggers:

  1. Entry (stretch open): trigger: section, start: "top bottom", end: "top top", scrub: 1. In onUpdate, set the h1's scaleY = targetScale * self.progress — the word grows from 0 to full viewport height as the section travels up into view.
  2. Pinned (collapse): trigger: section, start: "top top", end: "+=" + window.innerHeight + "px", pin: true, pinSpacing: false, scrub: 1. In onUpdate, set scaleY = targetScale * (1 - self.progress) — the word squashes back to 0 over one extra viewport of scroll. Because pinSpacing is false, the next section slides up and covers the pinned one while it collapses.
Section 3 — stretch in, then 10x blow-up reveal
  1. Entry: identical pattern — trigger: ".sticky-text-3", start: "top bottom", end: "top top", scrub: 1, onUpdate sets the h1 scaleY = targetScales[2] * self.progress.
  2. Pinned mega-zoom: trigger: ".sticky-text-3", start: "top top", end: "+=" + (window.innerHeight * 4) + "px", pin: true, pinSpacing: true, scrub: 1. All choreography lives in onUpdate, mapped to self.progress (0→1 across 4 viewport heights), manipulating the whole .text-container (not the h1) via inline styles:
  3. Scale phase, progress 0 → 0.75: scaleProgress = progress / 0.75; apply transform: scale3d(s, s, 1) with s = 1 + 9 * scaleProgress (i.e. 1 → 10). For progress > 0.75 clamp at scale3d(10, 10, 1).
  4. Backdrop fade, progress 0.25 → 0.5: the container's background-color stays fully opaque rgba(17, 39, 11, 1) below 0.25, then its alpha lerps 1 → 0 linearly across [0.25, 0.5] (alpha = 1 - (progress - 0.25) / 0.25, clamped 0..1), and stays alpha 0 above 0.5 — revealing the full-bleed photo underneath.
  5. Text fade, progress 0.5 → 0.75: the container's opacity lerps 1 → 0 across [0.5, 0.75] (opacity = 1 - (progress - 0.5) / 0.25); stays 0 above 0.75.
  6. At progress 0 reset: background back to opaque dark, opacity back to 1.
  7. Headline word reveal, progress 0.75 → 0.95: map textProgress = (progress - 0.75) / 0.2. For each SplitText word at index, show it with a hard cut (no fade tween): opacity = textProgress >= index / totalWords ? 1 : 0 via gsap.set(word, { opacity }) — words pop in sequentially left-to-right. Below 0.75 force all words opacity: 0; above 0.95 force all opacity: 1.

No eases, durations or staggers apply anywhere — every value is a pure linear function of ScrollTrigger progress with scrub: 1 providing ~1s of smoothing/lag.

Assets / images

One image: a full-bleed background photograph (landscape orientation, roughly 16:9 or wider, moody/atmospheric works well) that fills the third section behind the dark plate and is revealed as the plate fades out. It is displayed with object-fit: cover inside an absolutely positioned full-viewport wrapper.

Behavior notes

  • The whole page hijacks native scroll via Lenis; ScrollTrigger updates on Lenis's scroll event.
  • Target scales recompute on resize so words always stretch exactly to viewport height.
  • Sections 1–2 use pinSpacing: false (next section overlaps them); section 3 uses pinSpacing: true and consumes 4 extra viewport heights of scroll.
  • Total flow: hero → word 1 stretches/collapses → word 2 stretches/collapses → word 3 stretches, zooms 10x, dark plate fades, photo reveals, headline appears word-by-word → outro.