The scroll reveal that fired at the wrong time

I added two pauses to a scroll animation and quietly broke the one thing the animation was there to do. The section pins while you scroll, and each panel's headline and copy fade in as that panel locks to center. It looked right. Then I gave the timeline a beat to breathe at the top and a beat at the tail, so the pin stopped feeling like it snapped on and off. Next scroll through, the last panel's copy was just gone at rest, a blank slot where the headline should be, and then it flashed in for half a second while the section was already scrolling away.

Nothing errored. No warning, no red in the console. The reveals were all still attached. They just landed in the wrong place on the page.

If your scroll reveals fire at the wrong scroll position, the fix is almost always to stop positioning them as fractions of the whole animation and start firing them off the actual step, because the moment you add a pause anywhere in a scroll timeline it stops being linear, and every fraction you hard-coded now points at the wrong spot.

Why does a scroll reveal fire at the wrong scroll position?

The common way to reveal content inside a pinned, scroll-scrubbed section is to hang each reveal off the parent timeline at a fraction of its length: fire this fade at 30 percent, that one at 60 percent. That mapping carries a silent assumption, that scroll distance and timeline progress move together at a constant rate. Linear time. As long as the whole section is one smooth scrub, the assumption holds and the fractions land where you expect.

The instant you insert a hold, a beat where the scroll advances but the content deliberately does not, the timeline goes piecewise. Some of the scroll distance now buys a pause instead of progress. Every fraction you set against the old linear length now resolves to a different scroll position than it did before. In my case each reveal shifted about one panel late, and the fractions near the end walked right off the edge of where the pin releases.

Why did only the last panel go fully blank?

The earlier reveals drifted too, but "one beat late" is survivable, the copy still arrived while the panel was on screen, just a touch behind. The final reveal had no slack to give. Its mapped position now sat past the point where the section unpins, so at rest the panel was parked at zero opacity, and the fade only played during the exit ride as the scroll blew through the leftover timeline. Worse, the first and last panels are exactly the faces a visitor sees while the section is unpinned in either direction. Gating those two on a mid-timeline trigger is the one place you can least afford to, because they are visible when the timeline that controls them is not even running.

The surface said it worked. The meaning had moved.

Here is the part worth keeping. The reveals were never broken in the way "broken" usually means. The triggers existed, the code ran, the animation object was healthy. Every surface signal was green. What changed underneath was semantic: where in the scroll each beat actually happens. I had encoded a real assumption, the timeline is linear, into a pile of magic-number fractions, and then I changed the shape of the timeline without ever going back to those numbers, because nothing pointed at them. There was no line of code that said "these positions assume linear time." The assumption lived only in my head, and it expired the moment I added a pause.

That gap between surface and semantic is the failure mode I keep meeting in AI-built work, and it is worth naming because it does not show up as an error. The AI wires the reveals correctly against the timeline as it exists today. It has no way to know that "position 0.6" secretly means "assuming equal scroll per beat," so when a later change violates that, the reveal is still technically attached and technically wrong. The generator flatters the surface. Only a human watching the actual behavior (is the right copy visible while the panel is at rest?) catches the semantic drift.

How do you stop scroll reveals from drifting?

Stop positioning reveals by fractions of the container, and drive them off the active step instead. Fire a panel's reveal when that panel becomes the current step, play it once, and never reverse it. Add an onUpdate failsafe so a scrollbar drag, a keyboard jump, or a deep link that lands mid-section still resolves to the correct state instead of a half-played fade. And never opacity-gate the edge panels, the first and last, since those are the ones on screen while the section is unpinned. Index-driven reveals do not care whether the timeline is linear or piecewise, because they key off "which panel is active," which stays true no matter how many pauses you add.

The deeper move is to treat the assumption as a thing that can break. If the correctness of an effect depends on the shape of your timeline, linear versus piecewise, that dependency is an invariant, and an invariant you never wrote down is one a future change gets to violate for free. Either encode it where the next change will trip over it, or build the effect so it does not depend on the shape at all. Index-driven reveals took the second road: they removed the assumption instead of documenting it.

Questions that keep coming up

My reveal works until I add a pin or a pause, then it is off. Why? Because your reveal positions are fractions of a timeline you just made non-linear. The pause consumes scroll without advancing content, so every fraction now resolves to a later scroll point than it used to. The reveal is not broken, its coordinate system moved.

Should I just nudge the trigger offsets until it looks right? That patches the panel you are looking at and quietly breaks another, because the offsets are all wrong by different amounts once the timeline is piecewise. You are hand-correcting the output of bad position math instead of fixing the math. Re-key the reveals to the step index and the nudging stops.

How do I catch this before a visitor does? Watch the behavior, not the wiring. Scroll the section slowly in both directions and assert the obvious human thing at every rest point: the copy that should be visible is visible. A test that only checks "the trigger is attached" passes on exactly this bug.

If you're scaling personal site work into client builds and want a sparring partner on the production jump, /work-with-us.

// part of the ai websites topic

// grab the free starter kit that makes your AI stop forgetting and stop guessing: get it →

// building with AI? the field manual has the structured lessons.

// hitting this on a real build? this is what I fix →