All components

Gooey Blur Text Reveal

GSAP animation component · Published 2026-09-05 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

Line-by-line text reveal that reads like ink pooling into letters: each line is split with SplitText and wrapped in an inner span that starts at a large em-based blur and animates to sharp, while the outer line carries an SVG feColorMatrix alpha-threshold filter that hardens the soft edges into a gooey mass. Three modes per block (on load, once on scroll, scrubbed), Lenis smooth scroll, and banner photos that settle from a slight zoom as they pass.

How it's built

Categorytext
Techgsap, lenis
GSAP pluginsSplitText, ScrollTrigger
Complexitypage
Performance costmedium
Mobile-safeyes
Scrollhijacks scrolling

text reveal gooey blur splittext scrolltrigger lenis svg-filter

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

Gooey Blur Text Reveal — SplitText Lines Sharpening Under an Alpha-Threshold SVG Filter

Goal

Build a long editorial page (hero → banner photo → two paragraphs → banner → five-line index → banner → paragraph → banner → outro) where every heading and paragraph materialises line by line out of a soft blob. Each line is wrapped in an inner span that starts at a large blur (0.35em) and animates to sharp; the outer line carries an SVG feColorMatrix alpha-threshold filter, which hardens the blurred alpha into a solid, gooey mass — so instead of a fade the letters *pool* into shape, like ink. Three trigger modes per block: on load (with delay), once on scroll (enter the viewport), and scrubbed (tied to scroll between two points). Banner photos settle from a slight zoom as they pass. GSAP SplitText + ScrollTrigger, Lenis smooth scroll.

Tech

Vanilla HTML/CSS/JS with ES module imports. gsap (npm) with SplitText and ScrollTrigger, plus lenis. Fonts: Anton (headings), DM Sans (base).

Layout / HTML

<div class="gb">
  <svg class="gb-filter" aria-hidden="true"><defs>
    <filter id="gb-goo" x="-50%" y="-50%" width="200%" height="200%">
      <feColorMatrix in="SourceGraphic" type="matrix" values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 255 -140" />
    </filter>
  </defs></svg>

  <section class="gb-hero"><h1 class="gb-copy" data-mode="load" data-delay="0.8">The weight of old light</h1></section>
  <div class="gb-banner"><img src="img1.jpg" alt="" /></div>
  <section class="gb-room">
    <h3 class="gb-copy" data-mode="scroll">Before the camera there was only the patient hand…</h3>
    <h3 class="gb-copy" data-mode="scroll">A room of long shadow…</h3>
  </section>
  <div class="gb-banner"><img src="img2.jpg" alt="" /></div>
  <section class="gb-index">
    <div class="gb-copy" data-mode="scrub"><h1>Ground pigment</h1><h1>Falling shadow</h1><h1>Warm flesh</h1><h1>Suspended gesture</h1><h1>Held silence</h1></div>
  </section>
  <div class="gb-banner"><img src="img3.jpg" alt="" /></div>
  <section class="gb-collection"><h3 class="gb-copy" data-mode="scrub">We collect the paintings that hold their breath…</h3></section>
  <div class="gb-banner"><img src="img4.jpg" alt="" /></div>
  <section class="gb-outro"><h1 class="gb-copy" data-mode="scroll">Step closer</h1></section>
</div>

The alpha row 0 0 0 255 -140 multiplies alpha by 255 and subtracts 140: anything below ~55% alpha vanishes, anything above becomes solid — the threshold that makes blur look gooey.

Styling

:root: --gb-paper:#f4f3ef, --gb-ink:#17181a.

  • .gbbackground:var(--gb-paper); color:var(--gb-ink); font-family:"DM Sans". .gb-filterposition:absolute; width:0; height:0; pointer-events:none.
  • h1, h3margin:0 0 2rem; font-family:"Anton"; font-weight:400; line-height:1.02; text-transform:uppercase; letter-spacing:.005em; h1 { clamp(2.2rem,10vw,6.5rem) }, h3 { clamp(1.5rem,6.5vw,4rem) }.
  • sectionposition:relative; width:100%; min-height:100svh; padding:4rem. Hero/outro centre their text, h1 { width:50% }. .gb-index — centred column.
  • .gb-bannerwidth:100%; height:100svh; overflow:hidden; img object-fit:cover; will-change:transform.
  • .gb-line (SplitText line) — display:block; filter:url(#gb-goo) blur(0.4px); -webkit-filter: url(#gb-goo) blur(0.4px); will-change:filter. .gb-line__innerdisplay:inline-block; will-change:filter.
  • @media (max-width:1000px): section padding 2rem, hero/outro h1 { width:100% }.

GSAP effect (be exact)

gsap.registerPlugin(SplitText, ScrollTrigger). Build in mount(config)destroy().

const DEFAULTS = { blurStart: .35, duration: 1.5, stagger: .1, bannerZoom: 1.15, scrubStart: 75 };

Lenislenis = new Lenis(); lenis.on("scroll", ScrollTrigger.update); gsap.ticker.add(t => lenis.raf(t * 1000)); gsap.ticker.lagSmoothing(0).

Inside a gsap.context(() => { … }, root):

Per .gb-copy blockmode = data-mode || "load", delay = data-delay || 0; targets = the block itself if it is an h1/h3, otherwise its children. For each target: SplitText.create(target, { type: "lines", linesClass: "gb-line" }); for each line create <span class="gb-line__inner">, move the line's child nodes into it, append it to the line; collect the inners.

  • gsap.set(inners, { filter: "blur(0.35em)" }).
  • base tween { filter: "blur(0em)", ease: "power3.out", stagger: .1 }:
  • scrub → add scrollTrigger: { trigger: block, start: "top 75%", end: "bottom 75%", scrub: true }.
  • scrollduration: 1.5, delay, scrollTrigger: { trigger: block, start: "top 80%", once: true }.
  • loadduration: 1.5, delay.

Bannersgsap.fromTo(img, { scale: bannerZoom }, { scale: 1, ease: "none", scrollTrigger: { trigger: img.parentElement, start: "top bottom", end: "bottom top", scrub: true } }).

destroy()ctx.revert() (kills the ScrollTriggers and reverts the splits), remove the ticker callback, lenis.destroy().

Assets / images

Four full-bleed landscape photos (≥1600px wide): natural scenes with soft tonal range (canopy, garden path, lake, forest). They are dividers between text sections.

Behavior notes

  • The blur is in em, so the effect scales with type size; heavy faces work best — thin strokes fall below the alpha threshold and break up.
  • The threshold filter sits on the *line*, the animated blur on the *inner span*: that separation is the effect. Putting both on one element gives a plain blur.
  • Lenis owns page scrolling; if the host page already has Lenis, reuse that instance and keep the ScrollTrigger.update wiring once.
  • Only the prefixed variables are global.

Images

This component ships with 4 reference assets, served publicly. Use them as-is to reproduce the demo faithfully, then swap in your own — the layout expects the same aspect ratios.

https://motionprompts.dev/c/gooey-blur-text-reveal/img1.jpg
https://motionprompts.dev/c/gooey-blur-text-reveal/img2.jpg
https://motionprompts.dev/c/gooey-blur-text-reveal/img3.jpg
https://motionprompts.dev/c/gooey-blur-text-reveal/img4.jpg

They are hotlinkable for prototyping. For anything you ship, replace them: they are licensed for demonstration of this component, not for redistribution.

Using this outside its demo page

This component is written as a complete page — that is how the demo is meant to look. If you are dropping it into an existing project, or combining it with other components, these are the things it declares at document level and that you need to move or reconcile first.

  • Palette on :root--gb-paper, --gb-ink. These names are not namespaced and they collide: --ink is defined by 164 of the 219 components in this catalogue, --paper by 94, --muted by 80, each with different values — and they will also collide with whatever your own project defines. Move them onto the component's wrapper (.my-section { --ink: … }) or rename them with a prefix.
  • Rules on html, body — the demo owns the whole document, so these set the page background, typography and resets. Dropped into an existing project they restyle the entire page, not just this section. Re-target them at the component's wrapper before using it.
  • Smooth scroll (Lenis) — this creates its own Lenis instance, and a page may only have one. If your project already runs Lenis, drop the setup shown above and reuse the existing instance, keeping the lenis.on("scroll", ScrollTrigger.update) wiring once. Two instances fight over the same scroll and stutter visibly, with no error in the console.