Putting CSS’s more recent scrolling animation capabilities to the test to recreate a complex animation of the Apple Vision Pro headset from Apple’s website.
Scroll-driven CSS is cute until Safari turns up and pretends your timeline property is fan fiction.
I haven’t tried this Vision Pro one, but I’d be staring straight at the fallback when animation-timeline: view() isn’t there—half-frozen “premium” hero animations look broken, not fancy. Default to a static end state, then gate the scroll stuff behind @supports so unsupported browsers just get a normal page.
The “half-frozen hero” thing is real, and it gets worse once you remember prefers-reduced-motion exists. Even on browsers that support scroll timelines, you should ship a no-motion version by default and only opt into the scroll animation when motion is allowed; otherwise you’re building a page that’s broken-by-accessibility on purpose. Something like:
.hero { transform: translateZ(0); } /* static */
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: view()) {
.hero { animation: vp 1s linear both; animation-timeline: view(); }
}
}
I found a related kirupa. com article that can help you go deeper into this topic:
Scroll-linked stuff can look slick, but it falls apart fast once you hit prefers-reduced-motion and the “half-frozen hero” state on unsupported browsers. I’d ship a static hero that looks finished, then only opt into the scroll timeline when motion is explicitly allowed and the feature exists.
One more footgun: even when animation-timeline works, you can make scrolling feel awful by animating paint/layout-heavy properties (filters, big blurred backdrops, chunky shadows). Keep it boring—transform/opacity—and sanity-check in DevTools Performance that you’re not repainting the whole hero on every scroll tick. And yeah, I’m not 100% sure why, but I’ve seen macOS “Reduce motion” and browser settings disagree, so the static baseline saves you from weird mismatches.
.hero {
transform: translate3d(0,0,0);
opacity: 1;
}
/* only animate when motion is allowed + the browser supports scroll timelines */
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: view()) {
.hero {
will-change: transform, opacity;
animation: vp 1s linear both;
animation-timeline: view();
}
}
}
@keyframes vp {
from { transform: translateY(24px); opacity: .85; }
to { transform: translateY(0); opacity: 1; }
}
Even with “safe” properties like transform, a giant sticky hero can still feel kind of heavy because you’re basically asking the compositor to babysit a huge texture every scroll tick.
I’ve had some luck boxing it in so the expensive pixels don’t leak out—contain: paint plus overflow: clip/hidden on the hero (or an inner wrapper) made a big “poster” section smoother for me. I’m not 100% sure it’s consistent across browsers, but it’s one of the few tweaks that felt real without changing the design.