Scroll Snap Events

Adam Argyle
Adam Argyle

From Chrome 129 you can use the scrollSnapChange and scrollSnapChanging events from JavaScript. By implementing built-in snap events, the previously invisible snap state will become actionable, at the right time, and always correct. This is not a convenience you had without these events.

Before scrollSnapChange, you could use an intersection observer to find what element was crossing the scroll port, but determining what was snapped was restricted to a few circumstances. For example, you can detect if the snap items fill the scroll port or fill the majority of the scroll port. To do this you'd observe intersecting elements of the scroll area, then based on which item is consuming the majority of the scroll area, assume this was the snap target, then wait for scrollend and act upon the item snapped (the snap target).

Before scrollSnapChanging however, knowing when the snap target is changing or what it's being changed to (like given a scroll fling) was impossible.

A horizontal scroller is shown with numbered boxes inside as snap targets. On the left is a real time log of scrollSnapChange events, highlighting the snapTargetInline with blue. On the right is a real time log of scrollSnapChanging events, highlighting the snapTargetInline with gray.

Try it
https://codepen.io/web-dot-dev/pen/jOjaaEP

Great news, these new events make this information available quickly and easily. This unlocks scroll snap interactions to reach beyond their current capability and enables the orchestration of scroll snap relationships and fresh UI feedback scenarios.

scrollSnapChange

This event fires only if a scroll gesture has resulted in a new snap target having been rested on, and in the following order

  1. After the scroll has rested.
  2. Before scrollend.

This event fires just before scrollend, when scrolling is complete, and only if a new snap target was rested upon. This makes the event feel lazy or just-in-time when the scroll gesture has completed.

scroller.addEventListener('scrollsnapchange', event => {
  console.log(event.snapTargetBlock);
  console.log(event.snapTargetInline);
})

scroller.onscrollsnapchange = event => {
  console.log(event.snapTargetBlock);
  console.log(event.snapTargetInline);
}

The event exposes the snapped item on the event object as snapTargetBlock and snapTargetInline. If the scroller is horizontal only, the snapTargetBlock property will be null. The value of the property will be the element node.

Unique details for scrollSnapChange

Does not fire until the user releases their gesture

A finger still on the screen or fingers on a trackpad indicate the user's gesture is not finished scrolling, which means scroll has not ended, which means the snap target hasn't changed yet, it's pending a complete user gesture.

Does not fire if the snap target did not change

The event is for snap change, and if the snap target did not change, the event won't fire, even if the scroller was interacted with by a user. The user did in fact scroll though, so upon scroll completion the scrollend event still fires.

scrollSnapChanging

This event fires as soon as the browser has decided that the scroll gesture has or will result in a new snap target. It fires eagerly, and during scrolling.

scroller.addEventListener('scrollsnapchanging', event => {
  console.log(event.snapTargetBlock);
  console.log(event.snapTargetInline);
})

scroller.onscrollsnapchanging = event => {
  console.log(event.snapTargetBlock);
  console.log(event.snapTargetInline);
}

The event exposes the snapped item on the event object as snapTargetBlock and snapTargetInline. If the scroller is vertical only, the snapTargetInline property will be null. The value of the property will be the element node.

Unique details for scrollSnapChanging

Fires early and often during a scroll gesture

Unlike scrollSnapChange which awaits a complete user scroll gesture, this event will fire eagerly while a user scrolls with their finger or using a trackpad. Consider a user who is scrolling slowly without lifting a finger, scrollSnapChanging will fire multiple times during the gesture as long as the user is panning over multiple potential snap targets. Each time the user scrolls, if the browser has determined that upon release it would rest upon a new snap target, the event fires to tell you which element that is.

Does not fire all the snap targets along the way to a new snap target

Furthermore, consider a fling, where a user does a scroll throw gesture that spans multiple snap targets at once; this event will fire once with the target that will be rested upon. So it's eager but not wasteful, supplying you with the snap target as soon as possible.

Use cases and examples

These events enable many new use cases while also making current patterns much easier to implement. One prime example is enabling snap triggered animation. Contextually revealing the snap item, the snap item's children or associated information when it is the snap target.

The following patterns demonstrate some use cases to help you be productive straight away.

Highlight a testimonial

This example promotes or visually focuses the snapped testimonial.

scroller.onscrollsnapchange = event => {
  scroller.querySelector(':scope .snapped')?.classList.remove('snapped')
  event.snapTargetInline.classList.add('snapped')
}
https://codepen.io/web-dot-dev/pen/dyBZZPe

Show the caption for the snapped item

This example shows the caption for the snapped item. Both events are included in this demo, so you can see the timing and user experience differences between scrollSnapChange and scrollSnapChanging.

Snap Changing
https://codepen.io/web-dot-dev/pen/wvLPPBL

Snap Change
https://codepen.io/web-dot-dev/pen/QWXOObw

Animate once, the children of a snapped presentation slide

This example knows when a new slide has been landed and rested on, which is a great time to animate the contents once.

document.addEventListener('scrollsnapchange', event => {
  event.snapTargetBlock.classList.add('seen')
})
https://codepen.io/web-dot-dev/pen/rNEYYVj

Snapping on both x and y in a scroller

Scroll snap works for scrollers that allow horizontal and vertical scrolling. This demo shows both scrollSnapChanging and scrollSnapChange targets as you scroll around the grid. This demo illuminates how the element the browser is snapping to may not always be the one you think it is.

A grid of squares in a horizontal and vertical scroller is shown. The dashed border represents the scrollSnapChanging target and the solid border is the scrollSnapChange target. Red represents snapTargetInline, and blue represents snapTargetBlock.

https://codepen.io/web-dot-dev/pen/qBzVVdp

Two linked scrollers

This demo has two scroll snapping containers where one is a high level list of links and the other is the actual paged content. The new scrollSnapChanging event makes it trivial to link the snap states of these bi-directionally so they're always in sync.

https://codepen.io/web-dot-dev/pen/YzoEEXj

OKLCH color picker

This demo has 3 scrollers, each representing a different color channel in OKLCH. The snapped item is synchronized with its relevant radio group and the data can be retrieved from a form wrapping the inputs. To a mouse or touch user, you can scroll to the value you want. For keyboard users, you can tab and use the arrow keys. For a screen reader, it's just a form.

scrollSnapChanging is used to eagerly synchronize the snapped item with state, while scrollSnapChange is used to animate the affected color channel header that the user input was applied to.

https://codepen.io/web-dot-dev/pen/OJeOOVG

Snapping staggering animated hubs

This demo progressively enhances the scroll snapping experience with snap triggered transitions using scrollsnapchange.

Check for event support with the following JavaScript:

if ('onscrollsnapchange' in window) {
  // ok to use snap change
}
https://codepen.io/web-dot-dev/pen/MWMOOae

Scrollable ruler input

This demo features a scrollable ruler as an alternative way to pick a length for a number input. Enter values directly into the number input or scroll to the size. The changing event is used to clear the selection during the user's gesture, while the change event is used to update state and reinforce the user's choice.

https://codepen.io/web-dot-dev/pen/LYKOOpd

Cover flow

This demo builds upon Bramus Van Damme's excellent scroll driven animation recreation of the famous macOS cover flow (video tutorial too). Uniquely, scrollSnapChanging is used to hide the album title and scrollSnapChange is used to present the title. The events help orchestrate an eager hide of the old title and a lazy presentation of the new one.

https://codepen.io/web-dot-dev/pen/Bagmmog

More ideas to inspire some creativity

Now that it's trivial to know which element is about to be snapped and which is actively snapped, there are many new possibilities! Here's a list of ideas to help inspire creativity and additional use cases:

  • Triggering lazy loading, known as snapchange triggered rendering or data fetching.
  • Film strip thumbnails linked to a larger image.
  • Toggling play/pause for a video trailer for a snapped video thumbnail.
  • Analytics tracking
  • Scrollytelling
  • Wheel of Fortune UI/UX
  • Snap target gets an anchored tooltip.
  • Tap to snap
  • Snap to reveal
  • Sounds on snap
  • Swipe UI
  • Swipeable tabs or carousels

Further studies

The Chrome team is excited to hear what you build with these new APIs and hope it helps streamline your scrollable experiences.

Resources: