Your scroll reveals never finish, and you probably cannot see it

3 min readUpdated 22 August 2026

I shipped this site with a bug I could not see. Every heading, every paragraph, every case study body was wired to fade and rise as it entered the viewport. On the pages I checked while building, it worked. On the pages I did not, whole sections sat permanently at a quarter opacity behind a blur — legible only if you already knew what they said.

Nothing errored. The build was green. Every check I had was silent.

The mistake

The reveal was scrubbed against scroll position:

gsap.fromTo(
  el,
  { filter: 'blur(12px)', opacity: 0.25, y: 12 },
  {
    filter: 'blur(0px)',
    opacity: 1,
    y: 0,
    scrollTrigger: {
      trigger: el,
      start: 'top 85%',
      end: 'top 45%',
      scrub: true,
    },
  },
)

Read that end carefully. The animation completes when the element's top reaches 45% of the viewport height. That is a position the page has to be able to scroll the element into.

For a heading in the middle of a long page, fine. For anything in the last viewport of the document, the page runs out of scroll before the element ever gets that high — so progress stops at 0.6, or 0.3, and the element stays half-revealed forever. Same for anything inside a pinned section, where the element's position is frozen by design.

The result is not a broken animation. It is content that renders at 25% opacity and looks like a design choice.

Why it survives review

Three reasons this is unusually good at hiding:

It is position-dependent, not code-dependent. The same component works in one place and fails in another. There is no bad component to find.

Everything reports healthy. No console error, no failed request, no layout shift. Automated checks pass because the element is present, has non-zero size, and is technically visible.

You scroll past it. When you are building, you scroll fast and know what the text says. Your eye fills in what your screen is not showing.

I only caught it by measuring rather than looking:

// Anything still blurred after the page has settled
const stuck = [...document.querySelectorAll('body *')].filter((el) => {
  const blur = (getComputedStyle(el).filter || '').match(/blur\(([\d.]+)px\)/)
  return blur && Number(blur[1]) > 0.5
})
console.log(stuck.length, stuck.slice(0, 5))

That returned 49 on a cold load of the homepage.

The fix

A reveal is not a scrubbed animation. It is a one-shot that fires when the element arrives:

gsap.fromTo(
  el,
  { filter: 'blur(12px)', opacity: 0.25, y: 12 },
  {
    filter: 'blur(0px)',
    opacity: 1,
    y: 0,
    duration: 0.9,
    ease: 'power2.out',
    scrollTrigger: { trigger: el, start: 'top 88%', once: true },
  },
)

once: true and a fixed duration. The reveal always completes, because nothing about its completion depends on where the page can scroll to.

Scrubbing is the right tool when the element is permanently on screen and the scroll position genuinely is the input — a progress bar, a parallax layer, a pinned sequence. It is the wrong tool for anything that should end up at rest.

Two rules worth keeping

Author the resting state in the markup, animate away from it at runtime. If the hidden state lives in your CSS, then a JS failure, a slow chunk, or a reduced-motion branch leaves your content invisible rather than merely unanimated. Render it visible; let the animation take it away and put it back.

Write the test that says "nothing is still hidden." Not "the animation ran." Scroll each route end to end, then assert nothing remains blurred or transparent:

const stuck = await page.evaluate(() => /* as above */)
expect(stuck).toEqual([])

That check would have caught this on the first run. It is not running here yet, which is its own version of the problem.

The uncomfortable part

The build passed. TypeScript passed. Neither has any opinion about whether a paragraph is legible. The site was broken in a way that made it look unfinished to anyone who visited, and nothing in the pipeline was ever going to say so.

Green checks tell you the code does what you wrote. They cannot tell you that what you wrote was the wrong idea. For anything visual, at some point you have to open it and look — and better, measure the thing you are actually claiming.

Got something like this that needs sorting out?

Book a call