All components

Outline Text Filled by Media

GSAP animation component · Published 2026-08-02 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

A pinned stage where a giant word sits hollow — stroked, transparent fill — over a photograph, and a scroll-driven wipe uncovers a second copy of the same word filled with that photograph via `background-clip: text`. Two pixel-aligned copies rather than one element that changes: `-webkit-text-stroke` with a transparent fill and a clipped background are different paint paths, and any crossfade between them flickers on the sub-pixel edge of every letter, while two stacked copies with a `clip-path` wipe are exact, cheap and reversible. Both copies share a single grid cell and identical typography — nothing may differ but colour, stroke and clip, because a one-pixel drift shows the outline as a ghost beside the fill and the effect is dead. The fill image is set from JS off the background's own `src`, so the letters are in register with what is behind them and read as a window cut into the photograph. The wipe completes at 78% of the track so the finished word lands while the section is still on screen. Dressed as a dance season: Young Serif + Geologica, deep teal and bone.

How it's built

Categorytext
Techgsap, lenis
GSAP pluginsScrollTrigger
Complexitysection
Performance costlight
Mobile-safeyes
Scrollhijacks scrolling

scroll scrolltrigger sticky text-stroke background-clip outline-text image-in-text wipe typography lenis cinematic

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

Outline Text Filled by Media — the word is hollow until the image fills it

Goal

Build a pinned stage where a giant word sits hollow — stroked, transparent fill — over a photograph, and a scroll-driven wipe uncovers a second copy of the same word filled with that photograph, letter by letter, left to right.

Use it when the name of something has to *be* the image: a season title, a film title, a record sleeve.

Tech

Vanilla HTML/CSS/JS with ES modules: gsap + ScrollTrigger, and lenis.

Wire Lenis to ScrollTrigger — this is not optional:

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

Lenis animates its own scroll position; ScrollTrigger reads the native one. Without that first line the two clocks drift and the triggers fire at the wrong moment or not at all — the component looks broken in a way that never reaches the console. lagSmoothing(0) stops GSAP from swallowing a long frame, which on a scrubbed mechanic shows up as a jump.

The mechanic in one line

Two copies of the same word, pixel-aligned, one stroked and hollow and one filled via background-clip: text; a clip-path wipe uncovers the filled copy.

<div class="word" aria-label="Undertow">
  <span class="hollow" aria-hidden="true">Undertow</span>
  <span class="filled" aria-hidden="true">Undertow</span>
</div>

The four decisions that ARE this component

1. Two copies, not one element that changes.

-webkit-text-stroke with a transparent fill and a background clipped to text are different paint paths. Any crossfade between them flickers on the sub-pixel edge of every letter. Two stacked copies with a clip-path wipe are exact, cheap and perfectly reversible.

2. The alignment is the whole job.
.word { display: grid; }
.word span { grid-area: 1 / 1; /* identical everything else */ }

Both copies share one grid cell and identical typography. Everything that affects where a glyph lands — family, size, weight, line-height, letter-spacing, padding, white-space — must be identical in the two rules. Drift them by a single pixel and the outline shows as a ghost beside the fill: the effect is dead and it looks like a rendering bug.

What the filled copy *does* add is only paint, never metrics: background-image, background-size, background-position, background-clip: text, and the clip-path wipe. If you find yourself adding anything to the filled rule that is not in that list, it will move the text.

3. The fill image comes from the background's own src, set in JS.
filled.style.setProperty("--fill", `url("${bg.getAttribute("src")}")`);

Two reasons. The letters end up in register with what is behind them, so the word reads as a window cut into the photograph rather than as a word containing a different picture. And there is one place to change the photo instead of two.

4. The wipe finishes early — at 78% of the track.
const w = gsap.utils.clamp(0, 1, p / 0.78);

If it completes exactly as the section leaves, the reader never sees the finished word. The payoff has to land while the sentence is still on screen, and then hold. The last fifth of the track is that hold.

The light comes up with it

bg.style.opacity = 0.42 + w * 0.3;

Brightening the background in step with the wipe is what makes it read as light arriving rather than as a mask sliding across. It is one line and it changes the whole register of the effect.

Details

  • padding-block: 0.08em on the word: at clamp(3.2rem, 15vw, 13rem) the glyphs overflow their

box and clip against the ascenders.

  • aria-label on the wrapper, aria-hidden on both copies — otherwise the word is announced

twice.

  • -webkit-text-stroke has no universal standard equivalent. Where it is unsupported the hollow

copy is simply invisible and the filled one carries the section: degrade, not break.

Reduced motion

Show the word already filled (clip-path: none) and collapse the track. The image inside the letters is the point and it survives; only the wipe goes.

Adapting

Change the word (one word, up to nine or ten characters — the wipe needs width to read), the photograph, the palette, the type. A heavy serif or a fat grotesk both work; a light weight does not, because there is not enough counter area for the image to show. Keep: two copies, one grid cell, the fill sourced from the background, the early finish, and the background brightening in step.