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 pluginScrollTrigger(register it withgsap.registerPlugin(ScrollTrigger)).lenis(npm) for smooth scroll.split-type(npm, theSplitTypeclass) to split the paragraph into words.matter-js(npm) for the physics simulation (useEngine,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:
section.intro— contains:- a
navwith two<p>pill labels:MenuandLet's talk. - an
<h1>readingMotionprompts, centered. 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.
section.outro— contains an<h1>readingOne 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;. .introand.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;. Eachnav pis 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 pwithnew SplitType(".sticky p", { types: "words" })and collecttext.words. - Define the highlight keyword list:
["YouTube", "aesthetic", "immersive", "exceptional", "inspiration", "recreate", "void", "passionate", "PRO", "creativity", "life"]
wordsToHighlight= every split word whosetextContentincludes any keyword (substring match, soMotionpromptsPROandcreativity,match too). Setword.style.opacity = 1on each of them.
2. Matter.js setup
Engine.create({ gravity: { x: 0, y: 0 } })— gravity starts OFF. Create aRunnerandRunner.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 withgetBoundingClientRect()at build time). Setleft = x px,top = y px, and copy the word's computedcolor. - Create a Matter rectangle body centered at
(x + charWidth/2, y + charSpan.offsetHeight/2)with sizecharWidth × charSpan.offsetHeightand 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 acharBodiesarray (and the spans in acharElementsarray).
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 withtl.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 addphase2.to(word, { color: "#FFFFFF", duration: 0.1, ease: "power2.inOut" }, Math.random() * 0.9). Add withtl.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 = trueandengine.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.setPositionback to(initialX + element.offsetWidth/2, initialY + element.offsetHeight/2),Body.setAngle(body, 0), zero out velocity and angular velocity; clear the span'stransformand set itsopacity = 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#0F0F0Fsticky background, with the red#EB4330outro section revealed after the pin ends.