DEV Community

Cover image for JavaScript scroll snap events for scroll-triggered animations
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

JavaScript scroll snap events for scroll-triggered animations

Written by Abiola Farounbi✏️

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.

How are scroll snap events implemented?

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 direction
  • snapTargetInline — 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 direction

By 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:

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
});
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

Exploring unique use cases and demos

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

Carousel with precision snap control

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.

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");
});
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Auto-play video trailers on snap

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.

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();
  }
});
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

Dynamic scrollytelling

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");
  }
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

Addressing the gap between CSS and JS-driven snapping

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 element - inline axis — snapTargetInline references the snapped element - both axes — Both properties return the respective snapped elements

By 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.

Browser compatibility

Modern browsers are beginning to support these new events, but currently they’re limited to browsers with Chrome version 129 and above and Edge.

Browser compatibility for snapevent-api Sourced from caniuse.com

Comparison with Intersection Observer API

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

Conclusion and next steps

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!


Get set up with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.

NPM:

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
Enter fullscreen mode Exit fullscreen mode

Script Tag:

Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Enter fullscreen mode Exit fullscreen mode

3.(Optional) Install plugins for deeper integrations with your stack:

  • Redux middleware
  • ngrx middleware
  • Vuex plugin

Get started now.

Top comments (0)