Gemcook Tech Blog
🍮

【Chrome129】ScrollSnapイベントがやってくる!

2024/09/03に公開

はじめに

こんにちは!😄
Chrome129より、JavaScriptのScrollSnapに関するイベントを使用できるようになりますね!今回はこれらのイベントについてご紹介していきたいと思います。

https://developer.chrome.com/blog/scroll-snap-events?hl=ja&authuser=5

ScrollSnapとは

そもそも「ScrollSnapってなんやねん。」という方もいるかと思います。百聞は一見に如かずということで以下のような操作のことを指します。

cssプロパティscroll-snap-typeを指定することでScrollSnapが可能になります。上記の例では以下を適用させています。(詳しくはこちら)

scroll-snap-type: x mandatory;

スマホやタブレットの台頭に伴って、タッチ操作でスワイプさせる場面が増えてきました。ScrollSnapを導入することでUX向上につながります。

そして、Chrome129からScrollSnapが完了したときに発生するscrollSnapChangeイベントと、ScrollSnap中に発生するscrollSnapChangingイベントを使用することができます。今まではこれらのイベントがなかったため、Intersection Observer APIscrollendイベントを使用してスナップ状態を検知していました。しかし、これだとスナップされた要素の特定が限定的で、正確にスナップ状態を検知することが難しい状態でした。

scrollSnapChangescrollSnapChangingの登場により、適切なタイミングでスナップ状態を検知し、適切な操作を行うことが可能になります。それでは各イベントについて見ていきましょう!

scrollSnapChangeイベント

scrollSnapChangeイベントは、スクロールが止まった後、かつscrollendより前に発生するイベントになります。実際に試してみましょう。

import { useEffect, useRef } from "react";

export const Index = () => {
  const scrollerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const scroller = scrollerRef.current;

    const handleScrollSnapChange = () => {
      console.log("scrollsnapchange!!");
    };

    const handleScrollEnd = () => {
      console.log("scrollend!!");
    };

    scroller?.addEventListener("scrollsnapchange", handleScrollSnapChange);
    scroller?.addEventListener("scrollend", handleScrollEnd);

    return () => {
      scroller?.removeEventListener("scrollsnapchange", handleScrollSnapChange);
      scroller?.removeEventListener("scrollend", handleScrollEnd);
    };
  }, []);

  return (
    <div ref={scrollerRef}>
      <div>HTML</div>
      <div>CSS</div>
      <div>JavaScript</div>
      <div>Go</div>
      <div>Rust</div>
    </div>
  );
};

scrollsnapchangeイベント後にscrollendイベントが発生していることが分かりましたね!またユーザーがジェスチャーし続けている場合(スナップし続けている場合)は、scrollsnapchangeイベントは発生しないことも分かります。このようにscrollsnapchangeイベントを使用することによってスクロール/ユーザージェスチャーが完了していることを検知できます。

scrollSnapChangingイベント

scrollSnapChangingイベントは、スクロール操作によって新しいスタップターゲットが作成された場合発生するイベントです。実際に試してみましょう。

import { useEffect, useRef } from "react";

export const Index = () => {
  const scrollerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const scroller = scrollerRef.current;

    const handleScrollSnapChanging = (e) => {
      console.log("scrollsnapchanging to", e.snapTargetInline.innerHTML);
    };

    scroller?.addEventListener("scrollsnapchanging", handleScrollSnapChanging);

    return () => {
      scroller?.removeEventListener(
        "scrollsnapchanging",
        handleScrollSnapChanging
      );
    };
  }, []);

  return (
    <div ref={scrollerRef}>
       {/** 省略 */}
    </div>
  );
};

見ていただいて分かる通り、スナップターゲットが作成されるたびにscrollSnapChangingイベントが発生します。またユーザーがジェスチャーし続けている間でも、スナップターゲットが作成されるとイベントが発生しています。高速スクロールをした場合は、スクロールが完了するまでの間の要素(☝️で言うと「CSS・JavaScript・Go」)ではscrollSnapChangingイベントは発生せず、最後の要素(☝️で言うと「Rust」)のみで発生していることが分かりますね!

実際に使ってみる

scrollSnapChangescrollSnapChangingイベントを使って、UIを少しリッチにしてみました。

import { useEffect, useRef } from "react";
import "./style.css";

export const Index = () => {
  const scrollerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const scroller = scrollerRef.current;

    const handleScrollSnapChange = (e) => {
      scroller?.querySelector(":scope .snapped")?.classList.remove("snapped");
      scroller
        ?.querySelector(":scope .snapTarget")
        ?.classList.remove("snapTarget");
      e.snapTargetInline.classList.add("snapped");
    };

    const handleScrollSnapChanging = (e) => {
      scroller?.querySelector(":scope .snapped")?.classList.remove("snapped");
      scroller
        ?.querySelector(":scope .snapTarget")
        ?.classList.remove("snapTarget");
      e.snapTargetInline.classList.add("snapTarget");
    };

    scroller?.addEventListener("scrollsnapchange", handleScrollSnapChange);
    scroller?.addEventListener("scrollsnapchanging", handleScrollSnapChanging);

    return () => {
      scroller?.removeEventListener("scrollsnapchange", handleScrollSnapChange);
      scroller?.removeEventListener(
        "scrollsnapchanging",
        handleScrollSnapChanging
      );
    };
  }, []);

  return (
    <div ref={scrollerRef}>
      <div className="section__item">HTML</div>
      <div className="section__item">CSS</div>
      <div className="section__item">JavaScript</div>
      <div className="section__item">Go</div>
      <div className="section__item">Rust</div>
    </div>
  );
};
.section__item {
  /** 省略 */
  &:not(.snapped) {
    opacity: 0.25;
  }
  &.snapTarget {
    opacity: 0.6;
  }
}

スクロール中はスナップターゲットにはopacity: 0.6を適用し、それ以外の要素はopacity: 0.25を適用するようにしています。またscrollsnapchangeイベントでスクロール完了を検知するとスナップターゲットにはopacityを適用しないようにしています。

scrollSnapChangescrollSnapChangingイベントが発生するタイミングでopacityをかけているだけですが、今どれがスナップターゲットになっているか、スクロールが完了しているのかどうかが一目でわかるようになりましたね!

最後に

scrollSnapChangescrollSnapChangingイベントの登場により、スナップをトリガーとするアニメーションを作成することが簡単になります。以下のようなデモがいくつか紹介されており、ワクワクするようなUIの作成が可能になります!ぜひ一度触ってみてはいかがでしょうか!!

https://x.com/argyleink/status/1823783141846442091

最後までお読みいただき、ありがとうございました。

Gemcook Tech Blog
Gemcook Tech Blog

Discussion