Workshop · back to the section

Scroll-driven animation without JavaScript — and its trap

Fading sections in used to require a script. It no longer does — in exchange there's a failure mode that announces itself not with an error but with an empty page.

The established way of revealing things on scroll was long a script watching for an element to enter the viewport and adding a class. That works, but it has two drawbacks: a script has to run on the main thread, and the effect is independent of the pace of scrolling — it either started or it didn't.

That's what animation-timeline: view() is for. It doesn't trigger on an event; it ties the timeline itself to scrolling: the animation's state follows from where the element sits in the viewport. No script involved.

What you gain

No observer, no event handler, and none of the small stutter that JavaScript-driven versions can produce while scrolling. The browser handles the whole thing in its own animation layer.

And — more importantly — there's nothing to maintain. A CSS property doesn't go out of support the way a library does, and it needs no dependency.

Now the trap

I don't know this failure from theory: I found it on my own site, long after I thought it was finished.

The symptom is this. A visitor scrolls to the bottom, clicks through to another page, then presses back. The browser doesn't reload; it restores from memory — that's bfcache, and it exists precisely so that going back is instant.

But the scroll-linked timeline isn't necessarily recalculated then. The elements stay at their initial state. If that initial state is opacity: 0, the visitor returns to an empty page — while the content was in the document the whole time.

What happens when going back On a normal load the timeline starts and the content appears; restored from memory the timeline doesn't restart and the content stays at its initial, transparent state. normal load page loads back button from memory normal load timeline starts back button timeline idle normal load content visible back button empty page the content is present in the document in both cases
normal loadpage loads
back buttonfrom memory
normal loadtimeline starts
back buttontimeline idle
normal loadcontent visible
back buttonempty page

the content is present in the document in both cases

What makes it insidious is that it can't be reproduced by reloading. Anyone refreshing during development never sees it. The only real test is to scroll down, navigate away, and come back with the browser's back button.

The safety net, in two layers

The first layer restarts the animations at the moment of return: the pageshow event tells you whether the page came from memory, and removing then re-adding a class forces a recalculation.

The second layer is a guard that rescues the page even if the first wasn't enough: half a second after display it checks whether any section is in view yet transparent. If it finds one, it disables the animation for good. Better a page readable without animation than one beautifully empty.

The real lesson

bfcache isn't to blame, and neither is CSS. The fault is structural: I wrote an animation whose initial state hides the content. That holds for every such animation — if it fails to start for any reason, the content isn't partially degraded, it's gone.

Which is why there's now a floor on the animation: the starting value isn't zero but somewhat above it. The effect still reads, while the worst case is no longer an empty page but a fainter section. This isn't a retrofitted rescue but a principle — the failure state has to stay readable.

The same principle speaks elsewhere too: a failure is acceptable when it looks like a failure — silent breakage is the dangerous kind. That's also the subject of the piece on deploy traps, where the command runs green and the result is wrong anyway.

Questions on this topic

What is bfcache, and why does it break animations?

On the back button the browser doesn't reload the page but restores it from memory, complete with its state. That's fast, but the timeline of scroll-linked animations isn't necessarily recalculated — elements can stay at their initial state. If that initial state is transparent, emptiness is what's left where the content should be.

Why is an opacity: 0 starting value dangerous?

Because its failure mode isn't partial but total: the text doesn't get fainter, it isn't there. If the animation fails to start for any reason, the visitor sees an empty page while the content is present in the document all along. That's why the starting value is better set above zero.

How do you test for this failure?

Not by reloading, because reloading hides it. Scroll to the bottom, navigate to another page, then return with the browser's back button. The failure is most visible on the lower half of the page, because that's where people scroll to before moving on.

Is the CSS-based approach worth it at all?

Yes, because there's no script on the main thread and no element observation. But only if the failure state stays readable. An animation whose failure produces an empty page isn't decoration, it's a risk.

← Back to the Workshop