Users of Chrome 129 and above can now access the new scroll snap events — scrollsnapchange
and scrollsnapchanging
. These new events give users unique and dynamic control of the CSS scroll snap feature.
scrollsnapchanging
This event is triggered during a scroll gesture when the browser identifies a new scroll snap target that will be selected when the scrolling ends. It is also referred to as the pending scroll snap target.
The scrollsnapchanging
event is triggered continuously as a user scrolls slowly across multiple potential snap targets on a page without lifting their finger. However, it does not fire if the user quickly flings through the page, passing over several snap targets in one scrolling gesture. Instead, the event is triggered only for the final target where snapping is likely to settle.
scrollsnapchange
This event is triggered only when a scroll gesture results in a new scroll snap target being settled on. It occurs immediately after the scroll has stopped and just before the scrollend
event fires.
Another unique behavior about this event is that it does not trigger during an ongoing scrolling gesture which means that the scroll has not ended and it’s also likely that the snap target has not yet changed.
Scroll snap events work in conjunction with CSS scroll snap properties. These events are typically assigned to a parent container that contains the scroll snap targets.
Both of these JavaScript scroll snap events share the SnapEvent
object, which includes two important properties that are used in these events:
snapTargetBlock
— Provides a reference to the element snapped in the block direction when the event is triggered. If snapping only occurs in the inline direction, it returns null
as no element is snapped to in the block directionsnapTargetInline
— Provides a reference to the element snapped in the inline direction when the event is triggered. If snapping only occurs in the block direction, it returns null
as no element is snapped to in the inline directionBy using these properties together with scroll snap events and event handler functions, you can easily identify the element that has been snapped to and customize it or apply predefined styles as needed. In this article, we will explore how to achieve this.
Let’s take a look at a quick example of how to use these events:
See the Pen
Using scrollSnapChange and scrollSnapChanging Demo by coded_fae (@coded_fae)
on CodePen.
This example demonstrates a scrolling container with vertical scroll snapping applied to a group of grid boxes. These boxes, represented as div
elements, serve as the scroll snap targets.
Initially, all the boxes have a white background. During a scroll action, when a new snap target is pending, the background color changes to green. Once the snap target is selected, the background transitions smoothly to red with white text.
Here’s how the events are implemented. The comments in the code provide a step-by-step breakdown of the process:
For scrollsnapchanging
:
scrollContainer.addEventListener("scrollsnapchanging", (event) => { // This adds an event listener to the scroll container for the "scrollsnapchanging" event // This event fires during scrolling, predicting potential snap targets const previousPending = document.querySelector(".snap-incoming"); // Find any existing element with the "snap-incoming" class if (previousPending) { previousPending.classList.remove("snap-incoming"); // If such an element exists, remove the "snap-incoming" class // This ensures only one element has this active state at a time } event.snapTargetBlock.classList.add("snap-incoming"); // Add the "snap-incoming" class to the new snap target updateEventLog("Snap Changing"); // Update the event log to show that the snapchanging event has occured });
For scrollsnapchange
:
scrollContainer.addEventListener("scrollsnapchange", (event) => { // This adds an event listener to the scroll container for the "scrollsnapchange" event // The event fires when a scroll snap has been completed and a new target has been selected const currentlySnapped = document.querySelector(".snap-active"); // Finds any existing element with the "snap-active" class if (currentlySnapped) { currentlySnapped.classList.remove("snap-active"); // If such an element exists, remove the "snap-active" class } event.snapTargetBlock.classList.add("snap-active"); // Adds the "snap-active" class to the new snap target updateEventLog("Snap Changed"); // Update the event log to show that the snapchange event has occured });
These events offer unique use cases for scroll-triggered animations, as they allow precise control over animations during active scrolling (in progress) and upon reaching a snap target.
Let’s take a look at some unique cases for these events
This use case features horizontally scrolled carousels implemented using CSS scroll snap properties, with an additional feature that animates the contents of each slide when it becomes the current snap target.
This implementation is done by using the scrollsnapchange
event to dynamically apply a snap-active
style class directly to the current carousel slide. This smoothly adds animation to the carousel thereby creating a better engaging presentation.
See the Pen
Carousel with precision snap control by coded_fae (@coded_fae)
on CodePen.
Here is the step-by-step process of how the scrollsnapchange
event is used:
// Select the carousel container const carousel = document.getElementById("carousel"); // Get all slides within the carousel const slides = carousel.querySelectorAll(".carousel-slide"); // This adds an event listener to the scroll container for the "scrollsnapchange" event carousel.addEventListener("scrollsnapchange", (event) => { // Get the target slide that snapped into place (horizontally scrolled) const snapTarget = event.snapTargetInline; // This part updates the current slide for the navigation buttons const slideWidth = carousel.clientWidth; currentSlide = Math.round(carousel.scrollLeft / slideWidth); // Remove 'snap-active' class from previously active slides const currentlySnapping = document.querySelector(".snap-active"); if (currentlySnapping) { currentlySnapping.classList.remove("snap-active"); } // Add 'snap-active' class to newly snapped slide to trigger animations snapTarget.classList.add("snap-active"); });
In CSS, the snap-active
class is combined with the element’s existing class name. When the scrollsnapchange
event triggers and adds this class to the element’s class list, the corresponding CSS rules are immediately applied, dynamically updating the slide’s appearance and animating its content in real-time:
.carousel-slide.snap-active { opacity: 1; scale: 1; } .carousel-slide.snap-active .content { opacity: 1; transform: translateY(0); }
This use case functions similarly to how users interact with Instagram Reels, YouTube Shorts, and TikTok videos. It consists of a series of short videos that play automatically when snapped into view and pause when transitioning to another video.
By using the scrollsnapchanging
and scrollsnapchange
events simultaneously, the implementation precisely controls video playback, ensuring that only the currently visible video is playing while others remain paused.
See the Pen
Auto-Play Video Trailers on Snap by coded_fae (@coded_fae)
on CodePen.
Here is how the events were used:
const snapContainer = document.querySelector(".snap-container"); const trailers = document.querySelectorAll("video"); snapContainer.addEventListener("scrollsnapchange", (event) => { // Get the video element of the currently snapped item const visibleTrailer = event.snapTargetBlock.children[0]; if (visibleTrailer) { // Play the visible trailer when it snaps into view visibleTrailer.play(); } }); snapContainer.addEventListener("scrollsnapchanging", (event) => { const visibleTrailer = event.snapTargetBlock.children[0]; if (visibleTrailer) { // Pause the currently visible trailer when transitioning to another snap visibleTrailer.pause(); } });
In this use case, the video element is not directly nested within the snapTargetBlock
. Therefore, it is necessary to access the children
of snapTargetBlock
to access the video element:
const visibleTrailer = event.snapTargetBlock.children[0];
This use case highlights a dynamic and interactive page that showcases different “snap animations”. These animations are triggered as sections snap into view, and when a section is scrolled out of view, its corresponding animations are smoothly removed. This approach creates a unique and engaging scrollytelling experience.
In the demo below, we have a landing page divided into four distinct sections, each featuring unique animations that are revealed as you scroll. This is achieved using the scrollsnapchanging
and scrollsnapchange
events, which dynamically add or remove the animations based on the scrolling behavior.
How the logic works:
const scrollContainer = document.querySelector(".container"); // Handle the scrollsnapchange event scrollContainer.addEventListener("scrollsnapchange", (event) => { // Get the current snap target block const snapTarget = event.snapTargetBlock; // Add the "active" class to the first child element (if it exists) if (snapTarget.children[0]) { snapTarget.children[0].classList.add("active"); } // Add the "active" class to the second child element (if it exists) if (snapTarget.children[1]) { snapTarget.children[1].classList.add("active"); } }); scrollContainer.addEventListener("scrollsnapchanging", (event) => { // Get the current snap target block const snapTarget = event.snapTargetBlock; // Remove the "active" class from the first child element (if it exists) if (snapTarget.children[0]) { snapTarget.children[0].classList.remove("active"); } // Remove the "active" class from the second child element (if it exists) if (snapTarget.children[1]) { snapTarget.children[1].classList.remove("active"); } });
In a similar pattern to the earlier implementation, we need to access all child elements to apply different animations to them individually.
Now, let’s take a look at the CSS for the portfolio section:
@keyframes slideDown { 0% { opacity: 0; transform: translateY(-100%); } 100% { opacity: 1; transform: translateY(0); } } @keyframes slideUp { 0% { opacity: 0; transform: translateY(100%); } 100% { opacity: 1; transform: translateY(0); } } .portfolio-content.active { animation: slideDown 1s ease-out forwards; } .portfolio-grid.active img { animation: slideUp 1s ease-out forwards; } .portfolio-grid.active img:nth-child(1) { animation-delay: 0.2s; } .portfolio-grid.active img:nth-child(2) { animation-delay: 0.4s; } .portfolio-grid.active img:nth-child(3) { animation-delay: 0.6s; }
We created two different animations: slideup
and slidedown
. These animations were added to the active
class alongside the respective element’s class name.
To target individual child elements, we utilized the:nth-child()
pseudo-class. This approach allowed us to create a smooth animation flow.
As far back as 2022, scroll snapping relied solely on CSS for defining snap points and styling snap targets. While CSS provides a clean and declarative way to enable snapping through properties like scroll-snap-type
and scroll-snap-align
, it lacks dynamic control over the snap behavior and styling.
This gap becomes evident in more complex scenarios, such as implementing carousels or galleries where JavaScript is often needed to manage state, track interactions, and apply custom styles.
The introduction of JavaScript scroll snap events bridges this gap. These events provide real-time predictions and dynamic interactions with snap targets, enabling the addition of custom behaviors and animations that go beyond what CSS alone can achieve.
For instance, you can use scroll-snap-type
to control the snapping behavior and the scroll-snap-align
to determine the relevant properties returned by the SnapEvent
object:
block
axis — snapTargetBlock
references the snapped elementinline
axis — snapTargetInline
references the snapped elementboth
axes — Both properties return the respective snapped elementsBy combining CSS scroll snap properties with JavaScript scroll events, you can create an enhanced scrolling experience while maintaining smooth, intuitive interactions, as demonstrated in the examples above.
Modern browsers are beginning to support these new events, but currently they’re limited to browsers with Chrome version 129 and above and Edge.
Before the introduction of these JavaScript scroll events, the Intersection Observer API was used to track elements as they crossed the scroll port and identify which element was the current snap target based on how much of the viewport was filled by the element.
However, this approach was limited and didn’t have real-time updates on when and how the snap target was changing, making it less effective for more complex scroll-driven interfaces.
Here’s a brief comparison of scroll snap events and the Intersection Observer API, outlining their key differences and advantages:
Feature | Scroll snap events | Intersection Observer API |
---|---|---|
Primary use case | Used to identify precise scroll snap points by tracking target changes during scroll interactions | Used to detect when an element enters, leaves or intersects with a specified viewport container |
Ease of implementation | Easier to implement for scroll-specific interactions | Requires more configurations and effort to set up but provides broader capabilities beyond scroll snapping |
Supported events | scrollsnapchanging scrollsnapchange |
Custom callbacks triggered on visibility changes |
Styling snap targets | Directly identifies and allows styling snap targets | Needs custom logic to determine snap targets |
Cross-browser suppor* | Limited; currently supported in Chrome 129+ and Edge (Chromium-based). Not yet available in Safari or Firefox | Broad support in modern browsers |
Identifying when a section has been snapped into view or is about to be snapped, and then customizing that section or adding additional functionality at that precise snap point, is a valuable when used efficiently.
This article introduce these events and provides a good understanding of how they can be used to implement unique use cases easily and without the need for complex logic.
Moving forward, you can build on this foundation to develop more advanced use cases and functionalities.
I hope you found this tutorial helpful! Feel free to contact me on X if you have any questions. Happy coding!
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 nowChartDB is a powerful tool designed to simplify and enhance the process of visualizing complex databases. Explore how to get started with ChartDB to enhance your data storytelling.
A comprehensive guide to deep linking in React Native for iOS 14+ and Android 11.x, including a step-by-step tutorial.
Explore React 19’s new features, including the compiler, automatic memoization, and updates to hooks like use() and useFormStatus.
Create a multi-lingual web application using Nuxt 3 and the Nuxt i18n and Nuxt i18n Micro modules.