All components

Explode Text On Scroll (Matter.js)

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

Open live demo ↗ Raw prompt (.md)

What it does

A pinned sticky paragraph whose highlighted keywords progressively recolor as you scroll, then shatter into individual characters that fall and bounce under Matter.js physics. Driven by a scrubbed GSAP ScrollTrigger timeline that toggles gravity past 60% progress and syncs each character element to its physics body via an afterUpdate transform.

How it's built

Categoryscroll
Techgsap, lenis, split-type, matter-js
GSAP pluginsScrollTrigger
Complexitypage
Performance costheavy
Mobile-safedesktop-first
Scrollhijacks scrolling

scroll physics matter-js text gsap scrolltrigger split-type experimental explode

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

Explode Text On Scroll (Matter.js physics)

Goal

Build a scroll-driven text effect: a full-screen pinned paragraph starts invisible (text is the same color as the background), its words progressively light up in red as you scroll, then the highlighted keywords turn white — and at 60% of the pinned scroll they shatter into individual characters that fall and bounce on the floor with real Matter.js physics. Scrolling back up reverses everything and re-assembles the text.

Tech

Vanilla HTML/CSS/JS with ES module imports (Vite-style npm imports). Use:

  • gsap (npm) plus the GSAP plugin ScrollTrigger (register it with gsap.registerPlugin(ScrollTrigger)).
  • lenis (npm) for smooth scroll.
  • split-type (npm, the SplitType class) to split the paragraph into words.
  • matter-js (npm) for the physics simulation (use Engine, Runner, World, Bodies, Body, Events).

No images and no canvas rendering — Matter.js runs headless and drives DOM spans via CSS transforms.

Wire Lenis into GSAP the standard way:

  • lenis.on("scroll", ScrollTrigger.update)
  • gsap.ticker.add((time) => lenis.raf(time * 1000))
  • gsap.ticker.lagSmoothing(0)

Run everything inside DOMContentLoaded.

Layout / HTML

Three stacked full-viewport sections:

  1. section.intro — contains:
  2. a nav with two <p> pill labels: Menu and Let's talk.
  3. an <h1> reading Motionprompts, centered.
  4. section.sticky — contains a single long <p> (this is the section that gets pinned). Use this exact paragraph text (it must contain the highlight keywords listed below):

> Motionprompts is a YouTube channel where you learn to code modern web designs using HTML, CSS, JavaScript, React and NextJS. Focused on creating aesthetic interfaces, web animations, and immersive experiences, Motionprompts draws inspiration from the captivating websites showcased on platforms like Awwwards and Godly. With a keen eye for design, Motionprompts crafts elements and interfaces that exhibit a similar taste, often employing the power of GSAP and ScrollTrigger. In a nutshell, Motionprompts is all about coding elite web designs. In 2017, inspired by the standout designs of Awwwards, Motionprompts emerged. Beyond merely admiring these designs, there was a vision: to recreate them. The channel identified a gap — although these designs were exceptional, there wasn't a clear method for many to replicate them. It was this void that Motionprompts sought to fill. From its very inception, the platform has dedicated itself to converting design marvels into structured, comprehensible guides for coders of all proficiency levels. Recently, MotionpromptsPRO was introduced, a subscription-based service tailored to the needs of passionate web designers. As a PRO member, you gain exclusive access to source code for each tutorial and monthly website templates. These resources are carefully curated to support and inspire your creativity, helping you take your web design skills to the next level. MotionpromptsPRO opens up a realm of opportunities for professional growth and empowers you to bring your ideas to life with ease.

  1. section.outro — contains an <h1> reading One subscription, endless web design., centered.

Styling

  • Google Font Space Mono (weights 400 and 700, plus italics), applied to body. * { margin:0; padding:0; box-sizing:border-box; }, body { overflow-x: hidden; }.
  • Every section: position: relative; width: 100vw; height: 100vh; padding: 2em; overflow: hidden;.
  • .intro and .outro: flex, center both axes.
  • Palette:
  • .intro: background #EBEAE4, text #0F0F0F.
  • .outro: background #EB4330 (red-orange), text #0F0F0F.
  • .sticky: background #0F0F0F.
  • Key trick: .sticky p { color: #0F0F0F; } — identical to the section background, so the paragraph is invisible until GSAP animates the word colors.
  • nav: position: absolute; top: 0; left: 0; width: 100vw; padding: 2em; display: flex; justify-content: space-between; align-items: center;. Each nav p is a pill: text-transform: uppercase; font-size: 12px; padding: 2px 8px; border-radius: 20px; background: #0F0F0F; color: #EBEAE4;.
  • .char (the physics character spans created in JS): display: inline-block; position: absolute; pointer-events: none; opacity: 0;.

GSAP effect (be precise)

1. Word splitting and highlight words
  • Split .sticky p with new SplitType(".sticky p", { types: "words" }) and collect text.words.
  • Define the highlight keyword list:

["YouTube", "aesthetic", "immersive", "exceptional", "inspiration", "recreate", "void", "passionate", "PRO", "creativity", "life"]

  • wordsToHighlight = every split word whose textContent includes any keyword (substring match, so MotionpromptsPRO and creativity, match too). Set word.style.opacity = 1 on each of them.
2. Matter.js setup
  • Engine.create({ gravity: { x: 0, y: 0 } }) — gravity starts OFF. Create a Runner and Runner.run(runner, engine) immediately.
  • Add one static floor: Bodies.rectangle(window.innerWidth / 2, window.innerHeight + 5, window.innerWidth, 20, { isStatic: true }) — a bar sitting just below the bottom edge of the viewport.
3. Per-character physics bodies

For each word in wordsToHighlight, split its textContent into characters. For each character:

  • Create a <span class="char"> with the character as text, position: absolute, and append it to .sticky (NOT inside the word).
  • Position it to exactly overlay its character within the word: charWidth = word.offsetWidth / chars.length, x = wordRect.left − stickyRect.left + charIndex * charWidth, y = wordRect.top − stickyRect.top (rects measured with getBoundingClientRect() at build time). Set left = x px, top = y px, and copy the word's computed color.
  • Create a Matter rectangle body centered at (x + charWidth/2, y + charSpan.offsetHeight/2) with size charWidth × charSpan.offsetHeight and options { restitution: 0.75, friction: 0.5, frictionAir: 0.0175, isStatic: true }. Add it to the world.
  • Keep a record { body, element, initialX: x, initialY: y } in a charBodies array (and the spans in a charElements array).

The spans stay at opacity: 0 (CSS) until the explosion.

4. Pinned, scrubbed master timeline

Create the master timeline with this ScrollTrigger:

trigger: ".sticky"
start: "top top"
end: "+=" + (window.innerHeight * 4) + "px"   // 4 viewport-heights of pinned scroll
pin: true
scrub: true

Master timeline content (total duration 4 timeline-seconds, scrubbed across the pin distance):

  • Phase 1 — words turn red (timeline position 0): build a nested timeline. Shuffle a copy of ALL words (Fisher–Yates). For each shuffled word add phase1.to(word, { color: "#EB4330", duration: 0.1, ease: "power2.inOut" }, Math.random() * 0.9) — i.e. every word flips to red in a 0.1s tween placed at a random time within the first ~1 second, producing a sparkling random-order colorization. Add with tl.add(phase1, 0).
  • Phase 2 — highlight words turn white (timeline position 1): build a second nested timeline. Shuffle a copy of wordsToHighlight (Fisher–Yates). For each add phase2.to(word, { color: "#FFFFFF", duration: 0.1, ease: "power2.inOut" }, Math.random() * 0.9). Add with tl.add(phase2, 1).
  • Padding: append tl.to({}, { duration: 2 }) — two seconds of empty timeline so the color phases finish by 50% progress and the last stretch of scroll is reserved for the explosion moment.
5. Explosion trigger (in ScrollTrigger onUpdate)

Track lastProgress to derive scroll direction, and a physicsEnabled boolean flag.

  • When self.progress >= 0.6, scrolling DOWN, and physics not yet enabled:
  • Set physicsEnabled = true and engine.world.gravity.y = 1.
  • Hide every highlight word (word.style.opacity = 0) and show every char span (element.style.opacity = 1; element.style.color = "#FFFFFF").
  • For each char body: Body.setStatic(body, false), Body.setAngularVelocity(body, (Math.random() − 0.5) * 0.25), Body.setVelocity(body, { x: (Math.random() − 0.5) * 5, y: −Math.random() * 5 }) — a small random upward/sideways kick with spin, then gravity takes over and they fall and bounce on the floor.
  • Simultaneously fade OUT all non-highlight words with gsap.to(nonHighlightWords, { opacity: 0, duration: 0.5, ease: "power2.out" }) so only the flying letters remain visible.
  • When self.progress < 0.6, scrolling UP, and physics is enabled: reset everything:
  • physicsEnabled = false, gravity.y = 0.
  • For each char body: Body.setStatic(body, true), Body.setPosition back to (initialX + element.offsetWidth/2, initialY + element.offsetHeight/2), Body.setAngle(body, 0), zero out velocity and angular velocity; clear the span's transform and set its opacity = 0.
  • Fade ALL words back in: gsap.to(word, { opacity: 1, duration: 0.5, ease: "power2.in" }) for each word.
6. Physics → DOM sync

Subscribe to Matter's Events.on(engine, "afterUpdate", ...). On every tick, if physicsEnabled, for each { body, element, initialX, initialY } compute the offset from the initial body center:

deltaX = body.position.x − (initialX + element.offsetWidth / 2)
deltaY = body.position.y − (initialY + element.offsetHeight / 2)
element.style.transform = `translate(${deltaX}px, ${deltaY}px) rotate(${body.angle}rad)`

This makes each character span follow its physics body (position + rotation) while falling and bouncing.

Assets / images

None. The whole piece is typography and solid color blocks.

Behavior notes

  • Desktop-oriented; the layout/measurement happens once on load (no resize handling needed).
  • The explosion is a one-shot state toggled by scroll direction: it fires once past 60% scrolling down, and fully resets (letters snap back, words fade in) once you scroll back above 60% going up. Scrubbing the color phases remains fully reversible at all times.
  • The floor is invisible; characters come to rest along the bottom edge of the pinned viewport.
  • Expect the characters to render in white (#FFFFFF) while flying, on the near-black #0F0F0F sticky background, with the red #EB4330 outro section revealed after the pin ends.