IntersectionObserver
V2.
If used incorrectly, scroll-linked animations suffer from lower frame rates and apparent jitter due to inefficiencies in implementation. Even the 2014 hot topic, the parallax scrolling effect, met its fair share of inefficiencies. Frederick et al (2015) reported “significant usability issues” and even motion sickness from websites with parallax scrolling.
Animations are not always to blame, because even sans effects or improper scroll event listeners still create headaches. Twitter responded to a rash of issues with laggy scrolling in 2011, leading the creator of jQuery John Resig to conclude expensive scroll event handlers are a bad idea.
IntersectionObserver
solution we detail below.
The latter is likewise a suitable long-term solution with significant adoption across major browsers, covering up to 97% of users. However, its exact specifications undergo minor refactoring periodically, leaving many deprecated scroll-snap-* CSS properties.
This article revolves around both classes of responses, with two takeaway tips: use Javascript observer patterns and use CSS where possible. We start with JavaScript workarounds and solutions. We then discuss CSS properties and how they can be used to simplify scroll-linked animations — deprecated properties to avoid, current and widely supported properties in use, and upcoming properties.
IntersectionObserver
is a durable technique that accomplishes just this. Roughly speaking, once a certain scroll position has been reached, the IntersectionObserver
event handler is executed. We describe in more detail and more precisely what it accomplishes below.
IntersectionObserver
null
, or any other DOM element the developer specifies.var intersectionOptions = { root: null, // use the viewport rootMargin: '0px', threshold: 1.0 } function intersectionCallback(entries, observer) { entries.forEach(entry => { if (entry.intersectionRatio >= 1) { console.log("Fully visible!"); } else { console.log("Not fully visible!"); } }); } var observer = new IntersectionObserver(intersectionCallback, intersectionOptions); var target = document.querySelector('#box'); observer.observe(target);The following is the corresponding HTML and CSS:
<html> <head> <title>IntersectionObserver Demo</title> <style> #box { margin-top:100% 0 100% 0; width:100%; height:90%; background-color:black; } </style> </head> <body> <div id="box"></div> </body> </html>View the CodePen here, using a supported browser, and try scrolling the black box fully into view and then partially or completely out of view. Whenever the box is fully scrolled into view, the console reads
Fully visible!
. Whenever the box is scrolled so that it’s no longer fully in view, the console reads Not fully visible!
.
IntersectionObserver
tells you when an item is fully present in the viewport but not if the item is actually visible. For example, the item may be fully transparent or occluded, resulting in imprecise statistics for ad exposure.
IntersectionObserver
V2IntersectionObserver
, IntersectionObserver
V2 will invoke a callback whenever the target overlaps with the viewport. Unlike before, V2 now features two additional configurations, with descriptions below taking nearly verbatim from the W3 spec for its interface:
trackVisibility
: if true, track changes in the target’s visibility. Tracking visibility is more expensive than tracking intersections with the viewport, so webpage performance can degrade significantly with this enabled. As a result, this V2-only option should be used only when necessary. A warning to this effect appears if you set visibility tracking without specifying a valid delay.
delay
: minimum number of milliseconds between observer notifications, with a minimum of 100ms if trackVisibility
is true.
To see V2 in action, set the two configuration options above when initializing your IntersectionObserver
. The returned IntersectionObserverEntry
will have a new boolean field, isVisible
, that you can then use.
The following is a sample intersection observer configuration. There are two sample boxes: one partially see-through, and the other is solid colored. Any time the solid-colored box is fully visible in the user viewport, the handler is triggered. Most of the below code is reproduced from above, with the few new lines commented:
var intersectionOptions = { root: null, rootMargin: '0px', threshold: 1.0, trackVisibility: true, // NEW delay: 100, // NEW } function intersectionCallback(entries, observer) { entries.forEach(entry => { if ( entry.intersectionRatio >= 1 && entry.isVisible // NEW ) { console.log(entry.target.id + " fully visible!"); // NEW } else { console.log(entry.target.id + " not fully visible!"); // NEW } }); } var observer = new IntersectionObserver(intersectionCallback, intersectionOptions); var targets = document.querySelectorAll('.box'); // NEW - id -> class selector + select all targets.forEach((target) => { // NEW - iterate over all boxes observer.observe(target); });The HTML and CSS is also mostly reproduced, with just a few lines of change.
<html> <head> <title>IntersectionObserver Demo</title> <style> .box { # NEW - change from id to class selector margin-top:100% 0 100% 0; width:100%; height:90%; background-color:black; } .transparent { # NEW - new class for transparent box opacity: 0.5; } </style> </head> <body> <div id=”box1” class="box"></div> <!-- NEW: change from id to class selector --> <div id=”box2” class=”box transparent”></div> <!-- NEW: half-transparent box --> </body> </html>View the codepen here, using a supported browser, and try scrolling the boxes fully into view and then partially or completely out of view. Whenever the solid-colored box is fully scrolled into view, the console reads
Box 1 fully visible!
. Whenever the box is scrolled so that it’s no longer fully in view, the console reads Box 1 not fully visible!
. Note that regardless of the transparent box’s presence in the viewport or not, you will also always see Box 2 not fully visible!
.
Although this gives us a visibility-robust variant of discrete scrolling events, this does not encompass scroll-linked animations that occur continuously as the user scrolls. One example is the parallax effect. Several workarounds exist if a scroll event handler is necessitated in these scenarios. These methods generalize to all high-frequency event handlers and focus on reducing the number of times a handler is invoked.
wait
milliseconds are clumped into a burst. This means that as long as events occur within wait
milliseconds, a burst can last indefinitely. With trailing mode, this means the event handler is never triggered. In other words, your website’s content may never be animated onto the webpage, if the user keeps scrolling.
Throttling focuses on the rate of execution and resolves the above pitfall. When an event is invoked repeatedly, the event handler is guaranteed to be executed every wait
milliseconds. This has immediate applications with autocomplete in search. However, this method’s pitfall is where debouncing thrives — if a burst starts and ends within wait
 milliseconds, that burst will not trigger the event handler. For example, if wait
is 1000ms
second, and the user takes 500ms
 to scroll past Section A of the webpage, Section A will never run its scroll-linked animation. With the right combination of display and position misfortunes, this could offset the parallax effect adversely.
Admittedly, the above adversarial example is contrived, but this is to illustrate the complementary nature of both throttling and debouncing. Picking a method is choosing a tradeoff, and neither method entirely trumps the other. See articles from CSS Tricks’ Chris Coyier and [guest author David Corbacho for more details, along with the possibility of requestAnimationFrame
 as a third alternative. As we discuss in the CSS section below, these continuous scrolling effects are either currently or will be replaceable by browser-optimized CSS properties.
scroll-snap-align: [none | start | end | center] [none | start | end | center]
Specify snap alignment for both the x and y directions. If only one value is specified, that option is applied to both directions. Google Web Developers illustrates this alignment and provides a carousel example.scroll-snap-type: none | mandatory | proximity
The none option is straightforward, and the mandatory option is what you may expect from snap scrolling, where all scrolling positions snap. However, proximity only snaps if the user has scrolled to a position within the vicinity of a snap scroll position. Otherwise, the scroll is unaffected.scroll-padding: <length> | <percentage>
This configures padding for the scroll area and behaves like typical padding properties. There is an analogous scroll-margin property. This is not specific to snap behavior but avoids issues like a sticky header obscuring content.scroll-snap-stop
Note that this property is considered “at-risk” by the W3C 2021 Editor’s Draft, meaning it may or may not be deprecated in the future.<html> <head> <title>Scroll Snapping Demo</title> <style> html, body, section { width:100%; height:100%; } body { scroll-snap-type: y mandatory; } section { scroll-snap-align: start; } section.black { background-color: black; } </style> </head> <body> <section></section> <section class="black"></section> <section></section> <section class="black"></section> <section></section> </body> </html>You can view the codepen here. Try scrolling up and down to see the scroll snapping in action. Scroll snapping is just one of many scroll-linked animations, but with its 93.8%+ coverage of the user base and continued rapid rise in support, it may be ready for prime time with your application.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowDiscover Float UI, a set of pre-made templates that leverage the power of Tailwind CSS to help developers create professional websites quickly.
Learn how to use React-Toastify in 2025, from setup to styling and advanced use cases like API notifications, async toasts, and React-Toastify 11 updates.
Discover open source tools for cross-browser CSS testing like Playwright and BrowserStack to catch rendering errors, inconsistent styling, and more.
With the introduction of React Suspense, handling asynchronous operations like data fetching has become more efficient and declarative.
One Reply to "How to use scroll-linked animations the right way"
This article saved my life! I was struggling with scroll events and I didn’t even know the existence of the Intersection Observer API… This made everything so much simpler. Thank you so much!