Skip to main content
Back to Blog CSS scroll reveal animation example

CSS Scroll Reveal Animations with animation-timeline

Tom Hermans

Table Of Contents

Scroll reveal animations have become a staple of modern web design. Elements fade in, slide up, or scale into view as users scroll down the page. Traditionally, this effect requires JavaScript libraries, intersection observers, and careful timing logic. But modern CSS has evolved to handle this natively, and the results are surprisingly elegant. That said, these effects come with important considerations around performance and user experience — see the word of caution at the end of this article.

The JavaScript Problem

For years, scroll reveals meant reaching for libraries like AOS, ScrollReveal, or writing custom intersection observer code. These solutions work, but they add weight to your bundle, create potential performance bottlenecks, and introduce another layer of complexity to maintain.

JavaScript-based scroll animations also require careful coordination between your CSS and JS. You’re managing state, adding and removing classes, calculating scroll positions, and handling edge cases. It’s a lot of moving parts for what should be a straightforward visual effect.

Enter Scroll-Driven Animations

CSS scroll-driven animations flip the model entirely. Instead of animations running on a time-based timeline, they progress based on scroll position or viewport visibility. The browser handles all the calculation and coordination natively, with better performance and less code.

The key property is animation-timeline: view(). This creates what’s called a view progress timeline, an anonymous timeline that tracks an element’s position relative to the viewport. As the element enters, moves through, and exits the viewport, the animation progresses accordingly.

This approach embodies the “use the platform” philosophy that has driven web standards forward. Rather than reaching for JavaScript libraries to paper over missing CSS capabilities, we’re using built-in browser features designed specifically for this purpose. Scroll effects are fundamentally about visual presentation tied to scroll position — that’s what CSS is for.

By expressing these effects in CSS rather than JavaScript, we get better performance through compositor-thread optimization, better maintainability with declarative code, and better separation of concerns between styling and behavior. The browser can optimize scroll-driven animations in ways that JavaScript implementations simply can’t match, because the browser knows the animation’s intent from the start.

When the platform provides the right tool for the job, use it.

The Core Pattern

The system starts with a single utility class that does all the heavy lifting. Apply .scroll-reveal to any element you want to animate, and the browser takes care of the rest.

.scroll-reveal {
  --reveal-opacity-from: 0;
  --reveal-opacity-to: 1;
  --reveal-translate-y: 100px;
  --reveal-translate-x: 0px;
  --reveal-scale: 1;
  --reveal-rotate: 0deg;
  --reveal-blur: 0px;
  --reveal-range-start: entry 10%;
  --reveal-range-end: cover 30%;
  --reveal-easing: cubic-bezier(0,.69,.57,.56);
  
  animation: scroll-reveal var(--reveal-easing) forwards;
  animation-timeline: view();
  animation-range: var(--reveal-range-start) var(--reveal-range-end);
}

@keyframes scroll-reveal {
  from {
    opacity: var(--reveal-opacity-from);
    transform:
      translateY(var(--reveal-translate-y))
      translateX(var(--reveal-translate-x))
      scale(var(--reveal-scale))
      rotate(var(--reveal-rotate));
    filter: blur(var(--reveal-blur));
  }
  to {
    opacity: var(--reveal-opacity-to);
    transform: translateY(0) translateX(0) scale(1) rotate(0deg);
    filter: blur(0);
  }
}

Everything is controlled through CSS custom properties. This makes the animation highly configurable without writing new classes or duplicating code. Change a variable, change the effect.

How animation-timeline Works

The animation-timeline: view() property is doing something clever. It’s creating a timeline that’s synchronized with the element’s visibility in the viewport rather than advancing based on time.

Traditional animations run from 0% to 100% over a duration like 2 seconds. Scroll-driven animations run from 0% to 100% over a scroll range. The animation-range property defines exactly when that animation should start and end.

In this case, entry 10% means the animation starts when the element is 10% into the viewport from the bottom. cover 30% means it completes when the element has covered 30% of its journey through the viewport. These keywords give you precise control over when effects trigger.

Utility Classes for Common Effects

Instead of creating separate classes for every animation variant, the system uses utility classes that override only the relevant variables. This keeps your CSS lean and composable.

.reveal-from-left {
  --reveal-translate-x: -60px;
  --reveal-translate-y: 0;
}

.reveal-from-right {
  --reveal-translate-x: 40vw;
  --reveal-translate-y: 0;
}

.reveal-scale {
  --reveal-scale: 0.7;
}

.reveal-blur {
  --reveal-blur: 10px;
}

.reveal-rotate {
  --reveal-rotate: 10deg;
}

Want an element to slide in from the left? Add both .scroll-reveal and .reveal-from-left. Need it to scale up too? Add .reveal-scale. The classes stack naturally, and the variables cascade as expected.

<section class="scroll-reveal reveal-from-left reveal-scale">
  <h2>Slides in from the left while scaling up</h2>
</section>

Per-Element Customization

The variable-driven approach shines when you need one-off customizations. Instead of creating new classes or overriding styles with specificity hacks, you can adjust variables inline.

<div
  class="scroll-reveal"
  style="
    --reveal-translate-y: 80px;
    --reveal-opacity-from: 0.3;
    --reveal-scale: 0.85;
    --reveal-range-end: cover 20%;
  "
>
  <h3>Custom reveal with inline overrides</h3>
</div>

This is powerful for content-managed sites where editors might want slightly different effects for different sections. The core animation stays consistent, but individual elements can have personality.

Staggered Animations

Lists and grids often benefit from staggered reveals where items appear in sequence rather than all at once. With traditional time-based animations, you’d use animation-delay to create this effect. But scroll-driven animations work differently.

Since the animation progress is tied to scroll position rather than time, animation-delay won’t create the staggered effect you might expect. Instead, we adjust the animation-range for each element, making them trigger at different points in the scroll:

[class*="stagger-"] {
  --reveal-easing: linear;
}

.stagger-1 { 
  --reveal-range-start: entry 0%;
  --reveal-range-end: cover 20%;
}

.stagger-2 { 
  --reveal-range-start: entry 10%;
  --reveal-range-end: cover 30%;
}

.stagger-3 { 
  --reveal-range-start: entry 20%;
  --reveal-range-end: cover 40%;
}
<div class="grid">
  <div class="scroll-reveal stagger-1">Item 1</div>
  <div class="scroll-reveal stagger-2">Item 2</div>
  <div class="scroll-reveal stagger-3">Item 3</div>
</div>

Each item starts its animation at a slightly later scroll position. The first item begins immediately when it enters the viewport (entry 0%), the second waits until 10% into the entry phase, and the third until 20%. This creates a cascading effect as the user scrolls, with each element revealing in sequence.

The linear easing works particularly well here, creating a smooth progression through the staggered items without any acceleration curves conflicting with the scroll-based timing.

This approach works well for a handful of elements, but there’s potential for a more elegant solution when staggering larger sets — perhaps through calculated ranges or a more systematic way to define progressive offsets.

The Animation Range Explained

Understanding animation-range is key to getting scroll reveals feeling right. The range uses named scroll timeline ranges that describe where the element is relative to the viewport.

The entry range covers when the element is entering the viewport from below. exit covers when it’s leaving from the top. contain is when the element is fully contained within the viewport, and cover is when the element covers the scrollport.

Each range accepts a percentage that further refines when the animation progresses. entry 0% is the exact moment the element’s edge touches the viewport bottom. entry 100% is when the element is fully inside the viewport. This granular control lets you create effects that feel responsive to scrolling without being jarring.

Performance Considerations

Because scroll-driven animations are declarative CSS, the browser can optimize them heavily. The animations run on the compositor thread when possible, avoiding main thread jank. This is the same optimization that makes transform and opacity animations so smooth.

The browser also knows about the scroll-driven nature of these animations, so it can make smart decisions about when to calculate and update them. This is more efficient than JavaScript solutions that run on every scroll event or intersection observer callback.

Accessibility and Reduced Motion

Users who have enabled prefers-reduced-motion shouldn’t see these animations. Respect user preferences by wrapping the scroll-reveal styles in a media query:

@media (prefers-reduced-motion: no-preference) {
  .scroll-reveal {
    /* animation styles here */
  }
}

Without the animation, elements should appear in their final state immediately. The content should be fully accessible and readable regardless of animation support or user settings.

Easing and Feel

The default easing uses cubic-bezier(0,.69,.57,.56), which creates a smooth ease-out effect. The animation starts with energy and settles into place. This feels natural for scroll reveals because the user’s scroll action provides the initial momentum.

You can customize this per-element by overriding --reveal-easing. Want a bouncier effect? Use an elastic easing. Prefer linear? Just set it to linear. The variable system makes experimentation easy.

Combining Effects

The real power comes from combining multiple effects. An element can fade in, slide up, scale, rotate, and blur all at once. The transform properties stack in the keyframe definition, and each effect contributes to the overall animation.

<article 
  class="scroll-reveal reveal-scale reveal-blur"
  style="--reveal-rotate: 5deg;"
>
  <h2>Multiple effects combined</h2>
</article>

This element will fade in, scale up, blur from soft to sharp, and rotate slightly as it enters the viewport. All with one class and minimal inline customization.

Browser Support and Fallbacks

Scroll-driven animations are relatively new. As of 2026, they’re well-supported in modern browsers, but older environments won’t recognize animation-timeline: view().

For graceful degradation, ensure elements without animation support are still visible and in their final state. You can use @supports to add the animation only where it’s available.

@supports (animation-timeline: view()) {
  .scroll-reveal {
    /* scroll-driven animation styles */
  }
}

This progressive enhancement approach means the feature works everywhere, but users with modern browsers get the enhanced experience.

When to Use This Pattern

Scroll reveals work best for content sections, card grids, feature lists, and anywhere you want to draw attention as users explore your page. They’re particularly effective for storytelling sites, portfolios, and marketing pages where pacing matters.

Avoid overusing them. Not every element needs to animate in. Save reveals for important content or to establish visual hierarchy. Subtle is often better than spectacular.

The Demo

View on Codepen (opens in a new tab)

update February 7, 2026

Another cool demo I created a few weeks ago.

View on Codepen (opens in a new tab)

The Developer Experience

From a maintenance perspective, this pattern is clean. New animations don’t require new classes, just new variable values. The core animation logic lives in one place. Updates propagate automatically.

For designers and content creators, the inline customization option means they can tweak effects without touching stylesheets. This separation of concerns keeps everyone working efficiently.

Building On This Foundation

This utility system is a starting point. You could extend it with more directional classes, more easing presets, or different animation effects entirely. The variable-driven architecture makes it straightforward to add new capabilities without breaking existing uses.

You could even build a visual editor that lets users preview different scroll reveal settings and generates the appropriate HTML and CSS. The declarative nature of the approach makes tooling easy.

Why This Matters

Scroll-driven animations represent a shift in how we think about CSS. The language is becoming more declarative and powerful, handling use cases that previously required JavaScript. This isn’t just about saving kilobytes, though that’s nice. It’s about aligning the solution with the problem domain.

Scroll effects are fundamentally about visual presentation tied to scroll position. That’s what CSS is for. By expressing these effects in CSS, we get better performance, better maintainability, and better separation of concerns.

Final Thoughts

With animation-timeline: view(), scroll reveal animations become a first-class CSS feature. Add .scroll-reveal, tweak a few variables, and let the browser handle the rest. No libraries, no observers, no coordination logic.

This is the kind of CSS feature that makes you rethink what’s possible. Scroll-driven animations are just the beginning. As the specification evolves, we’ll see more timeline types and more creative uses of this powerful primitive.

Try building your own scroll reveal system. Start simple, add utilities as needed, and discover what works for your projects. The declarative approach might just change how you think about animation on the web.

A Word of Caution

While this article demonstrates how to implement scroll reveals with modern CSS, it’s worth acknowledging the legitimate criticism these effects have received.

Developer David Bushell’s article “Death to Scroll Fade” (opens in a new tab) articulates frustrations many share about generic, heavy-handed scroll reveal implementations.

The main concerns are valid: cognitive overload from constant animation, poor performance across different devices and platforms, and negative impacts on Core Web Vitals, particularly Largest Contentful Paint. Sites that fade everything on the page create a distracting experience that slows down both page performance and user comprehension.

The key is restraint. Scroll reveals should be used sparingly, not applied to every paragraph, list item, or element on the page. Individual lines of text don’t need to fade in. Reserve these effects for meaningful content blocks where the animation genuinely adds value — hero sections, feature callouts, important visual moments.

Test on real devices, not just your development machine. What feels smooth on a high-end laptop might stutter on mid-range phones. Monitor your performance metrics and be prepared to dial back or remove animations if they’re hurting the user experience.

The existence of a CSS-based solution doesn’t automatically make scroll reveals a good idea. It just makes them easier to implement — for better or worse. Use this power responsibly.

animation-range, animation-range-start (name + %) animation-range-end (name + %) cover / contain / entry etc..

More information and resources

If you want to dive deeper into how animation-range works and what keywords like entry, exit, contain, and cover actually mean, check out the interactive View timeline range visualizer (opens in a new tab). It’s an excellent tool for experimenting with different animation-range-start and animation-range-end values and seeing exactly how percentage offsets affect when your animations trigger.

Visualizer + playground for scroll-driven animations (opens in a new tab)

Get more information on scroll-driven-animations (opens in a new tab)

Scroll Animations devtools - chrome extension (opens in a new tab)

Scroll Animations tutorial series by Bramus (opens in a new tab)

Back to Blog

Let's Talk

Tom avatar

Get in touch

I work at the intersection of design and code.
Interested? Hit me up.
tomhermans@gmail.com

Copyright © 2026 Tom Hermans. Made by Tom Hermans .

All rights reserved 2026 inc Tom Hermans