スクロール時に視差効果を構築する

スクロール時のパララックス効果とは、ユーザーがページを下にスクロールする際に、コンテンツの異なるレイヤが異なる速度で移動する視覚的な手法です。これにより、前景の要素が背景の要素よりも速く動いているように見え、奥行きがあるように感じられます。その結果、魅力的で没入感のあるブラウジング エクスペリエンスが実現します。この効果は、CSS スクロールドリブン アニメーションを使用すると最適に実現できます。これにより、アニメーションをコンテナのスクロール位置にリンクできます。

導入方法

基本的なパララックス効果を作成する方法は次のとおりです。

  1. ラッパー要素を作成する: この要素は、視差効果のすべてのレイヤをグループ化するだけのものです。スクロール可能な要素ではないため、オーバーフローはクリップする必要があります。また、視差効果のレイヤの 1 つの高さに一致する height を指定します。

    <div class="wrapper">
      …
    </div>
    
    .wrapper {
      overflow: clip;
      height: 100vh; /* Height of one of the layers of the parallax */
    }
    
  2. レイヤを宣言する: ラッパー内に、異なる速度で移動する個々のレイヤを追加します。

    <div class="wrapper">
      <div class="layer">LAYER 0</div>
      <div class="layer">LAYER 1</div>
      <div class="layer">LAYER 2</div>
      …
    </div>
    
  3. 移動アニメーションを追加する: レイヤの transform プロパティを変更する CSS アニメーションを定義します。視差効果を得るには、通常、translateY を使用してレイヤを垂直方向に移動します。

    @keyframes parallax {
      from {
        transform: translateY(700px);
      }
    }
    
  4. view-timeline を設定する: アニメーションをスクロール位置にリンクするには、ラッパー要素に view-timeline を作成し、レイヤに適用します。

    .wrapper {
      view-timeline: --wrapper;
    }
    
    .layer {
      animation: parallax linear both;
      animation-timeline: --wrapper;
    }
    
  5. アニメーションをずらす: レイヤを異なる速度で移動させるには、主に 2 つの方法があります。キーフレームをずらす方法と、animation-range をずらす方法です。

    どちらのアプローチでも、ハードコードされた値を使用することも、sibling-index() / sibling-count() 実装を使用することもできます。ハードコードされた値は最も簡単で、レイヤの数が限られている場合にも便利です。sibling-index()/sibling-count() の実装は、レイヤが多数ある場合に便利です。

    • キーフレームのスタッガリング:

      ハードコードされた値を使用して、各レイヤのカスタム プロパティを定義し、視差オフセットを手動で制御できます。

      .layer:nth-child(1) { --offset: 100px; }
      .layer:nth-child(2) { --offset: 200px; }
      .layer:nth-child(3) { --offset: 300px; }
      
      @keyframes parallax {
        from {
          transform: translateY(var(--offset));
        }
      }
      

      sibling-index() を使用して、sibling-index() 関数が兄弟要素の中の子要素のインデックスを返すようにして、スタッガード効果を自動的に計算します。

      @keyframes parallax {
        from {
          transform: translateY(calc(100px * sibling-index()));
        }
      }
      
    • animation-range をずらす:

      ハードコードされた値を使用すると、各レイヤの animation-range の境界を個別に明示的に定義できます。

      .layer:nth-child(1) { animation-range: entry 25% exit 50%; }
      .layer:nth-child(2) { animation-range: entry 25% exit 75%; }
      .layer:nth-child(3) { animation-range: entry 25% exit 100%; }
      

      sibling-index() と sibling-count() を使用すると、レイヤの総数(sibling-count())に基づいて範囲を数学的に計算できます。

      .layer {
        animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
      }
      

コード例

@keyframes parallax {
  from {
    transform: translateY(calc(100px * sibling-index()));
  }
}

.wrapper {
  view-timeline: --wrapper;
}

.layer {
  animation: parallax linear both;
  animation-timeline: --wrapper;
}

@media (prefers-reduced-motion: reduce) {
  .layer {
    animation: none;
  }
}

または、animation-range プロパティを使用して同様の効果を得ることもできます。

@keyframes parallax {
  from {
    transform: translateY(700px);
  }
}

.wrapper {
  view-timeline: --wrapper;
}

.layer {
  animation: parallax linear both;
  animation-timeline: --wrapper;
  animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
}

@media (prefers-reduced-motion: reduce) {
  .layer {
    animation: none;
  }
}

ベスト プラクティス

スクロール駆動型アニメーションを使用する場合は、スムーズでアクセスしやすいエクスペリエンスを実現するために、いくつかのベスト プラクティスに従うことが重要です。

  • 推奨: 機能検出を含める。すべてのブラウザがスクロールドリブン アニメーションをサポートしているわけではありません。@supports ((animation-timeline: view()) and (animation-range: entry)) を使用してサポートを確認し、サポートしていないブラウザに代替を提供します。
    • 部分的にしかサポートしていないブラウザを除外するには、ここに (animation-range: entry) チェックを含める必要があります。
    • フォールバック戦略に scroll-timeline-polyfill パッケージを使用しないでください。このパッケージは機能が完全ではなく、既知の問題が多数あります。
    • アニメーションが装飾的なものにすぎない場合は、プログレッシブ エンハンスメントを選択し、代替手段を提供しないでください。
  • 推奨事項: ユーザーの設定を尊重します。ウェブ上でのモーションを好まないユーザーもいます。prefers-reduced-motion メディアクエリを使用して、これらのユーザーに対してアニメーションを無効にするか、アニメーションを減らします。
  • 推奨: パフォーマンスの高い CSS プロパティのみをアニメーション化する。アニメーションを最もスムーズにするには、ブラウザのコンポジタ スレッドで処理できるプロパティ(transform や opacity など)のみをアニメーション化します。width や height などの他のプロパティをアニメーション化すると、パフォーマンスの問題が発生する可能性があります。
  • 推奨: 正しい宣言順序を使用する。animation 略式プロパティを使用する場合は、略式プロパティがタイムラインをリセットしないように、animation-timeline と animation-range をその後に宣言します。

animation-range の設定については、次のとおりです。

  • 推奨: すべてのレイヤに同じ開始オフセット(entry 25% など)を指定します。
  • 推奨: すべてのレイヤに異なる終了オフセットを指定し、sibling-count() と sibling-index() を使用してオフセットを分散します(例: exit calc(100% / sibling-count() * sibling-index()))。

ブラウザのサポートとフォールバック戦略

そのため、通常はフォールバック戦略が必要です。

スクロール駆動型アニメーションをサポートしていないブラウザでは、フォールバックを使用して視覚効果を再現できます。フォールバックは通常、スクロール リスナー(ScrollTimeline 効果の場合)または IntersectionObserver API(ViewTimeline 効果の場合)を使用して構築されます。

スクロールドリブン アニメーションのサポートが組み込まれているブラウザでは、常にネイティブ CSS 実装を使用してください。その方がパフォーマンスが向上します。

フォールバック アプローチを使用してすべてのエフェクトを再現できるわけではありません。

このユースケースでは、次のスクリプトで、スクロール駆動アニメーションをサポートしていないブラウザにフォールバックを適用します。IntersectionObserver を使用して .wrapper 要素の可視性を追跡し、スクロール位置に基づいてレイヤの transform プロパティを更新します。

// Fallback for browsers that don't support scroll-driven animations
if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
  const wrapper = document.querySelector('.wrapper');
  const layers = document.querySelectorAll('.layer');

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        window.addEventListener('scroll', onScroll);
      } else {
        window.removeEventListener('scroll', onScroll);
      }
    });
  }, { threshold: 0 });

  observer.observe(wrapper);

  function onScroll() {
    const scrollY = window.scrollY;
    const wrapperRect = wrapper.getBoundingClientRect();
    const wrapperTop = wrapperRect.top + scrollY;
    const wrapperHeight = wrapperRect.height;
    const windowHeight = window.innerHeight;

    if (scrollY >= wrapperTop - windowHeight && scrollY <= wrapperTop + wrapperHeight) {
      const scrollPercent = (scrollY - (wrapperTop - windowHeight)) / (wrapperHeight + windowHeight);
      
      layers.forEach((layer, index) => {
        // This matches the effect as defined in the CSS example above.
        // Customize this further if needed.
        const initialTranslateY = 100 * index;
        const translateY = initialTranslateY * (1 - scrollPercent);
        layer.style.transform = `translateY(${translateY}px)`;
      });
    }
  }

  // Trigger onScroll once to set initial positions
  onScroll();
}