RSSAmplifier

Nick Paolini's Blog · May 13, 2026

Scroll-Driven Animations, With Taste

0
Sign in to vote or save

Nick Paolini · nickpaolini.com

In the Modern CSS Toolkit post I put scroll-driven animations in the "use sparingly" bucket and moved on. Three months later I want to come back to that, because the situation has changed and the conversation hasn't caught up.

Safari 26 shipped scroll-driven animations late last year. As of May 2026, Chrome, Edge, and Safari all support them natively. Firefox still has them behind a flag, but it's coming. We're at roughly 85% global support — comfortably in "progressive enhancement" territory.

So: should you use them? Yes. But the answer most tutorials give — here's the API, go forth and animate — is how the entire web ends up feeling like a slot machine. The hard part of scroll-driven animations was never the API. It's the editorial discipline. Just because every element can animate on scroll doesn't mean any of them should.

This post is the framework I use to decide. Plus five demos that let you feel where the line actually is.

The API in 90 Seconds

If you've never used scroll-driven animations, here's everything you need to follow along. Skip this section if you have.

There are two timelines:

/* Animate based on the page's scroll position */
.progress-bar {
  animation: fill linear;
  animation-timeline: scroll();
}
 
/* Animate based on an element entering/exiting the viewport */
.card {
  animation: fade-up linear;
  animation-timeline: view();
  animation-range: entry 0% cover 30%;
}

Notice there's no animation-duration. Time isn't the input anymore — scroll position is. The animation "scrubs" forward and backward as the user scrolls.

animation-range controls when in the timeline the animation plays. entry 0% cover 30% means "start when the element first appears in the viewport, finish when it's 30% into the visible area."

One Firefox quirk to know about: even with the flag enabled, Firefox requires animation-duration: 1ms for scroll-driven animations to apply. Add it. It's harmless in browsers that don't need it.

That's the whole API. The rest of this post is about restraint.

The Taste Framework

Before adding any scroll-driven animation, I ask three questions:

1. Does it carry information, or is it decoration?

A reading progress bar carries information (how far through the article are you). A card that fades up as it enters the viewport carries no information — the card is already visible, the fade is a flourish. Neither is wrong. But information-carrying animations earn their place automatically. Decorative ones have to argue for it.

2. Would a focused reader notice it?

The best scroll animations are the ones a reader doesn't consciously register. They feel like the page is behaving correctly. The moment a reader thinks "oh, that animated nicely," you've broken focus. That's a tax. Sometimes worth paying. Often not.

3. If you removed it, would anything be lost?

If the answer is "no, the page would still work and feel fine" — that's actually a strong argument for the animation, because it means you're enhancing without depending. If the answer is "yes, the experience would feel broken," that's a danger sign: you've made motion load-bearing, and the 15% of users on Firefox or with prefers-reduced-motion: reduce are getting a worse product.

Patterns That Earn Their Place

These are the scroll-driven animations I use in production without hesitation:

Reading progress indicator. Information-carrying. Subtle. Helps the reader understand commitment. The demo above is one.

Sticky header refinement. As the reader scrolls past the hero, the header gains a subtle shadow and slightly reduces in height. Communicates "you've left the top" without slapping the user.

Image zoom correction on entry. Images load at 102% scale and gently settle to 100% as they enter. Hides loading reflow, feels intentional. No one notices it consciously. Everyone feels it.

Subtle fade-up on long-form content. Subtle. 6–10 pixels of translation, opacity from 0.6 to 1.0, completes within 30% of viewport entry. If the user is scrolling fast they see no animation at all, just content. If they're reading thoughtfully it adds rhythm.

Sticky section labels. A category label that fades in when its section enters and fades out as it leaves. Acts as a wayfinding signal for long pages.

What these have in common: they all serve the reader's existing intent. They don't demand attention. They reward it.

Patterns That Ruin the Experience

Here are the moves I now actively refuse:

The "every element animates" anti-pattern. Every card, every paragraph, every image fading and translating as it enters. The page feels like a slot machine. The user can't read because nothing is ever stable.

Dramatic rotation/scale on entry. A card that rotates from 90 degrees to 0 and scales from 0.5 to 1 as it enters. The user's eyes have to track and re-recognize the content twice. Reading speed drops measurably.

Long-form parallax. Background images moving at half-speed of foreground. Looks beautiful in a Dribbble shot, makes long-scroll readers feel seasick within 30 seconds. The vestibular system did not evolve to enjoy this.

Letter-by-letter text reveal. I love this in a hero section, once, briefly. I do not love it on every heading on the page. It delays comprehension by hundreds of milliseconds per element, and on a content-heavy page that adds up to seconds the reader will resent.

Horizontal scroll hijacking. "Vertical scroll drives horizontal motion through this section." Sometimes brilliant for storytelling. Usually breaks scrollbar expectations, breaks keyboard navigation, breaks Ctrl+F, and infuriates anyone with a trackpad set to "natural" scrolling. Use surgically or not at all.

The Performance Lie Nobody Tells You

People will tell you scroll-driven animations are "free" because they run on the compositor. This is true if you animate the right properties.

Animate transform and opacity: compositor-only, 60fps even on a Chromebook, genuinely free.

Animate height, width, top, left, box-shadow, filter, background-position: layout or paint each frame. The compositor can't help you. On a low-end Android phone, you can absolutely jank a scroll animation.

/* ❌ Looks fine on your M3, ruins your scroll on a 2021 Pixel */
@keyframes grow {
  from { height: 0; box-shadow: 0 0 0 rgba(0,0,0,0); }
  to   { height: 200px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); }
}
 
/* ✅ Compositor-only, smooth everywhere */
@keyframes grow {
  from { transform: scaleY(0); opacity: 0; }
  to   { transform: scaleY(1); opacity: 1; }
}

If you must animate something layout-affecting, throttle yourself to one such animation visible at a time. Test on a mid-range Android device. If you don't have one, WebPageTest's "Moto G4" profile is the closest most of us will get.

prefers-reduced-motion Is Not Optional

This is the part I refuse to soft-pedal. If you ship scroll-driven animations without respecting prefers-reduced-motion, you are actively making your site worse for people with vestibular disorders, with attention disorders, with migraines. They've told their browser they don't want motion. Honor it.

The pattern I use:

.card {
  /* Default state — what reduced-motion users see */
  opacity: 1;
  transform: none;
}
 
@media (prefers-reduced-motion: no-preference) {
  .card {
    animation: fade-up linear;
    animation-timeline: view();
    animation-range: entry 0% cover 30%;
  }
}
 
@keyframes fade-up {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

Note the structure: the unanimated state is the default. The animation is added inside the media query, not subtracted from outside it. This guarantees that if anything about the animation breaks — old browser, JS error, polyfill failure, user preference — the content is still readable.

Pitfalls I've Hit

1. Forgetting animation-duration: 1ms for Firefox. Even behind a flag, Firefox needs it. It's not in most tutorials. I lost an hour to this last week.

2. Animating the page on first load. Scroll-driven animations fire on initial render if the element starts inside the viewport. If your hero card "fades up" the first time the page loads, that's a regular animation, not a scroll-driven one. Use animation-range: entry 0% entry 100% or similar to scope correctly.

3. view() timelines on position: sticky elements. They behave in surprising ways because the element's viewport position is being modified by the sticky behavior. If your sticky element's animation looks "wrong," this is almost always why. The fix is usually to put the animation on a non-sticky child or wrapper.

Decision Framework

Same three questions from the toolkit post, applied to this feature:

Browser support? Approximately 85% global. ⚠️ Use as progressive enhancement.

Fallback cost? If you structure it correctly (animation inside @media (prefers-reduced-motion: no-preference), default state is the unanimated state), the fallback is the static page. ✅ Excellent.

Eliminates complexity elsewhere? Replaces IntersectionObserver-based animation libraries (AOS, Framer Motion's scroll utilities, GSAP ScrollTrigger for simple cases). For complex orchestration, GSAP is still better. For 90% of "fade in on scroll" code, this kills the dependency. ✅

Verdict: Yes. With taste. With prefers-reduced-motion support. With a default state that doesn't depend on the animation working.

The Film-Score Test

Here's the heuristic I keep coming back to. A great film score does its job when you don't notice it. You leave the theater feeling moved, but you can't hum the music. The score served the story.

A bad film score is the one you remember instead of the movie.

Scroll-driven animations are a film score for your page. The reader is there for the content. Your job is to make the page feel right while they read — not to make them notice how cleverly the page is animating.

If your scroll animations are getting compliments, that might actually be a problem.

Bottom Line

The features are here. The browser support is good enough. The performance is real. None of that means you should use them everywhere.

Start with one: add a reading progress bar to your blog. That's the single highest-value, lowest-risk scroll-driven animation in existence.

Add a second: subtle fade-up on cards that's so subtle a non-designer can't tell if it's there.

Stop there for a month. Ship it. See if anyone notices. If they do, you've gone too far.

The web has been burned by motion design before. The Flash era. The parallax era. The "every element scrolls in" era. We have a chance to do this generation right. The constraint isn't technical — it's editorial.

What animation are you going to not add this week?

Resources


Related posts:

Read the original on nickpaolini.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.