カルーセル スナップ ハイライト

スクロール状態コンテナクエリを使用すると、要素が「固定」(固定配置)されているか「スナップ」(スクロール スナップ)されているかなど、現在のスクロール状態に基づいて要素のスタイルを設定できます。これにより、JavaScript の Intersection Observer やスクロール イベント リスナーに依存することなく、アクティブなアイテムを視覚的に区別できるカルーセルやギャラリーのエクスペリエンスを実現できます。

コア実装

スナップされたアイテムをハイライト表示するには、スクロール スナップ コンテナを確立し、スナップ ターゲットをスクロール状態コンテナとして定義してから、その状態をクエリして子孫のスタイルを設定する必要があります。

1. スクロール スナップ コンテナを確立する

親コンテナで scroll-snap-type が有効になっている必要があります。

<div class="carousel">
  <div class="carousel-item">
    <div class="card">Product 1 content</div>
  </div>
  <div class="carousel-item">
    <div class="card">Product 2 content</div>
  </div>
</div>
.carousel {
  display: flex;
  overflow-x: auto;
  /* MANDATORY: Enable scroll snapping on the container */
  scroll-snap-type: x mandatory;
}

2. スナップ ターゲットをスクロール状態コンテナとして定義する

スナップをトラッキングするカルーセル内の各アイテムは、scroll-state コンテナとして宣言する必要があります。

.carousel-item {
  /* Define where the item snaps within the container */
  scroll-snap-align: center;
  
  /* MANDATORY: Establish this element as a scroll-state query container */
  container-type: scroll-state;
}

3. snapped の状態をクエリする

コンテナクエリは子孫のスタイルを設定するため、スナップ ターゲット内の要素にハイライト スタイルを適用する必要があります。スクロール コンテナが X 軸でオーバーフローするように設定されているため、scroll-state(snapped: x) クエリを使用します。

必須: @media (prefers-reduced-motion: no-preference) でスタイルをラップして、アニメーションを減らすようリクエストしていないユーザーにのみ効果を表示します。ユースケースによっては、エフェクトの一部を保持することもできますが、この場合はカードが白から青に点滅し、一部のユーザーに問題を引き起こす可能性があるため、完全に無効にします。

/* Specify transition outside of queries so that it is applied regardless of state.  */
.card {
  transition:
    scale 0.4s cubic-bezier(0.25, 0.8, 0.25, 1),
    background-color 0.4s,
    color 0.4s,
    box-shadow 0.4s;
}
/* 
Only show the effect for users not requesting reduced motion. Disable completely, including the color change, as it causes a flash that may be problematic.
*/
@media (prefers-reduced-motion: no-preference) {
  /* Style the content when its parent .carousel-item is snapped on the x axis */
  @container scroll-state(snapped: x) {
    .card {
      background: #007bff;
      color: white;
      scale: 1.15;
      box-shadow: 0 10px 25px rgba(0, 123, 255, 0.3);
    }
  }
}

/* MANDATORY Copy-Paste Safety: Disable highlight scaling/flashing for motion sensitive users */
@media (prefers-reduced-motion: reduce) {
  .card {
    transition: none !important;
    scale: 1 !important;
  }
}

snapped 記述子は、特定の軸(x、y、inline、block、both)をクエリできます。

ユーザー補助

AVOID: インタラクティブ要素で scroll-state を使用する。

スナップされたアイテムを視覚的にハイライト表示すると UX が向上しますが、スナップされたアイテムはアクセシビリティ ツリーに公開されません。スナップされたアイテムに適用されるビジュアル テーマは、その要素がアクティブであることやフォーカスされていることを示すものであってはなりません。また、キーボード フォーカス リングは、snapped ハイライトとは明確に区別され、はっきりと見える必要があります。スナップされたアイテムがインタラクティブな場合は、他の標準的なユーザー補助機能を使用して、アクセスできるようにする必要があります。

スナップはスクロールによって発生しますが、キーボード フォーカスは移動しません。ただし、キーボード フォーカスによってスクロール コンテナが移動し、スナップされたアイテムが変更される可能性があります。このアイテムはフォーカスされたアイテムである場合とそうでない場合があります。これはユーザーの混乱を招く可能性があるため、推奨されません。

代替戦略

スクロール状態クエリをサポートしていないブラウザでは、アクティブなハイライトがなくても、すべてのアイテムが読み取れる機能的な基本エクスペリエンスを提供する必要があります。

特徴検出

@supports を使用すると、サポートされているブラウザにのみ拡張機能を提供できます。

@supports (container-type: scroll-state) {
  /* Enhancement styles here */
}

JavaScript フォールバック

ハイライト表示がユーザー エクスペリエンスにとって重要である場合は、IntersectionObserver を使用してスナップされたアイテムを特定します。rootMargin に負のインライン値を指定して、監視対象領域をカルーセルの中央の薄いスライスに調整します。たとえば、カルーセルの中央 2% にある要素を交差しているとみなすには、rootMargin を "0px -49%" に設定します。

// Optional: detect support and apply a JS-based fallback
if (!CSS.supports('container-type', 'scroll-state')) {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      // Toggle a class based on intersection
      entry.target.classList.toggle('is-snapped', entry.isIntersecting);
    });
  }, {
    root: document.querySelector(".carousel"),
    // Carousel item intersects if any part of the carousel item is in the middle 2% of the carousel.
    rootMargin: "0px -49%"
  });

  document.querySelectorAll('.carousel-item').forEach(item => {
    observer.observe(item);
  });
}