NP
← Back to Blog
CSSWeb DevelopmentFrontendAnimationUX

Scroll-Driven Animations, With
Taste

NP

Nick Paolini

May 13, 2026

9 min read read

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.

Demo 1: Reading Progress

Bar style:

The quick brown fox jumps over the lazy dog. This is sample text to demonstrate the reading progress indicator.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.

Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

More content here to make the container scrollable. The progress bar at the top tracks how far you've scrolled.

Keep scrolling to see the bar fill up completely. This is the canonical "good" scroll-driven animation.

Additional paragraphs to ensure scrolling. The reading progress indicator is subtle but informative.

You can see exactly how much content remains without being distracted from reading.

This pattern works for blog posts, documentation, and any long-form content.

Final paragraphs. Almost at the end now. The bar should be nearly full.

And that's the demo! Scroll back up to see it work in reverse.

This is the same pattern used at the top of the post you're reading. Scroll back up — that bar is doing this exact thing.

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.

Demo 2: The Taste Spectrum

Tasteful ✓

Scroll the container, then change the intensity. Same content, same scroll.

Card One

This is the first card with some sample content.

Card Two

Second card demonstrates the animation at your chosen intensity.

Card Three

Third card shows how the effect scales with the slider.

Card Four

Fourth card helps you feel where the line is.

Card Five

Fifth card makes the pattern clear across multiple elements.

Card Six

Final card completes the demonstration.

Most production scroll animations are at 5+. Most should be at 1–2. The line is real. You can feel 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.

Demo 3: Good vs Bad: Card Reveal

Subtle ✓

First Card

Content that should remain readable during animation.

Second Card

The difference in these two approaches is visceral.

Third Card

Subtle keeps content accessible throughout.

Fourth Card

Excessive makes content temporarily unreadable.

Fifth Card

Which would you rather experience?

Excessive ⚠

First Card

Content that should remain readable during animation.

Second Card

The difference in these two approaches is visceral.

Third Card

Subtle keeps content accessible throughout.

Fourth Card

Excessive makes content temporarily unreadable.

Fifth Card

Which would you rather experience?

Same content. Same scroll distance. The right side is what most tutorials teach you to build. Don't.

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.

Demo 4: Sticky Header Refinement

Style:

Sticky Header

Hero Section

Scroll down to see the header refine

Content Section 1

This is sample content to make the page scrollable. Watch how the sticky header subtly changes as you scroll past the hero.

Content Section 2

This is sample content to make the page scrollable. Watch how the sticky header subtly changes as you scroll past the hero.

Content Section 3

This is sample content to make the page scrollable. Watch how the sticky header subtly changes as you scroll past the hero.

Content Section 4

This is sample content to make the page scrollable. Watch how the sticky header subtly changes as you scroll past the hero.

Content Section 5

This is sample content to make the page scrollable. Watch how the sticky header subtly changes as you scroll past the hero.

Content Section 6

This is sample content to make the page scrollable. Watch how the sticky header subtly changes as you scroll past the hero.

Content Section 7

This is sample content to make the page scrollable. Watch how the sticky header subtly changes as you scroll past the hero.

Content Section 8

This is sample content to make the page scrollable. Watch how the sticky header subtly changes as you scroll past the hero.

⚠ Performance note: This animates height and backdrop-filter (non-compositor properties). Measured cost on a 2021 Pixel: ~3ms/frame.

Subtle communicates 'you've left the top.' Dramatic communicates 'something is happening at you.' The first is help. The second is noise.

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.

Demo 5: Reduced Motion Toggle

Simulate prefers-reduced-motion: reduce

Animated Header

Card 1

This card has scroll-driven animations when motion is enabled. With reduced motion, it appears immediately in its final state. The content is identical either way.

Card 2

This card has scroll-driven animations when motion is enabled. With reduced motion, it appears immediately in its final state. The content is identical either way.

Card 3

This card has scroll-driven animations when motion is enabled. With reduced motion, it appears immediately in its final state. The content is identical either way.

Card 4

This card has scroll-driven animations when motion is enabled. With reduced motion, it appears immediately in its final state. The content is identical either way.

Card 5

This card has scroll-driven animations when motion is enabled. With reduced motion, it appears immediately in its final state. The content is identical either way.

Card 6

This card has scroll-driven animations when motion is enabled. With reduced motion, it appears immediately in its final state. The content is identical either way.

Current mode: Motion enabled (animations active)

Toggle reduced motion. Notice that nothing is missing. That's the bar. If your reduced-motion experience is worse than 'nothing is missing,' you've made motion load-bearing.

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:

CSSWeb DevelopmentFrontendInteractive

I Told You to Wait. CSS Anchor Positioning Is Here.

7 min read
ReactNext.jsPerformanceWeb DevelopmentBest Practices

30 Days with the React Compiler: What I Stopped Memoizing

9 min read
AIWeb DevelopmentUXForm ValidationJavaScript

Building AI-Powered Form Validation: Beyond Basic RegEx

13 min read