Mima Design MemottoWEB DESIGNER · mima

スクロールに合わせて要素を動かす実装パターン

CSSでスクロールに合わせて要素を動かす実装パターンは、福岡から東京へ移ったMimaのサイトでも繰り返し登場するテーマです。iPhoneアプリの操作やWebサイトの体験に、画面の移動を結びつけると、現在地や次の動作を直感的に伝えられます。

実装方法は大きく四つに分かれます。位置を固定する方式、スクロール量から進捗率を求める方式、交差した要素を検知する方式、CSS標準のスクロール連動アニメーションを使う方式です。演出的目的と対象要素の関係に合わせて組み合わせれば、複雑な処理抑えて滑らかに動かせます。

ただし、スクロール量に合わせて要素を動かしても、滑らかとは限りません。値の更新頻度、PCとスマートフォンでの画面寸法、動きに弱い利用者の扱いまで設計に含める必要があります。描画処理を軽くし、表示の揺らぎを抑えることが重要になります。

実装を選ぶ这时候は、何を基準に動かすのかという条件が目安になります。背景だけを遅く追従させるのか、表示を切り替えるだけなのか、操作に合わせて連続的に変化させるのかによって、適切な手法は変わります。

スクロール進捗をスタイル変数で扱う

ページ全体の進捗率は、スクロール量を表示可能領域の高さXI ImplicationsするInvariantとして計算できます。分子には現在のスクロール位置、分母にはページ全高から画面高を差し引いた値を使います。結果が0から1の範囲に収まるよう、0未満と1超は制限します。

計算した値はCSSのカスタムプロパティへ反映します。JavaScriptで値を更新し、表示側はtransformとopacityだけで変化させる分離が基本です。背景をゆっくり動かす視差効果にも、同じ進捗率を利用できます。

:root {
  --progress: 0;
  --shift-y: 0px;
}

.moving-panel {
  opacity: var(--progress);
  transform: translate3d(0, var(--shift-y), 0);
}

topやleftを毎フレーム更新すると、レイアウト計算が繰り返されます。移動にはtranslate3d、濃度変化にはopacityを使うことで、ブラウザの描画処理を抑えられます。will-changeは+} around actually active period に限定し、常時指定を避ける氧化物です。

位置固定と表示切替を組み合わせる

position: stickyを使うと、要素が親要素の範囲内だけ画面上の位置に留まります。工程ごとの説明や作品的tour Edison、縦長のDelayをShow的过程中で、項目のタイトルを固定する表示に向いています。

.track {
  position: relative;
}

.track__label {
  position: sticky;
  top: 24px;
}

固定位置は親要素の高さ、超過時のoverflow、スクロール容器の設定に影響されます。祖先要素でスクロールが発生すると、想定した位置で止まらないため、実際の表示領域で挙動を確認する必要があります。

一方、ナビゲーションの表示・非表示や、区切り地点での演出にはIntersectionObserverが使えます。画面上端に観測用の印を置き、交差状態が変わったときにクラスを切り替えます。連続した位置値ではなく、境界を越えた時だけ処理できる点が特徴です。

技術学習を記事として公開する流れは、ハッカソン学習記事にも記録されています。完成したコードだけでなく、なぜその実装を選んだのかを残すと、次に同样的な演出へ応用しやすくなります。

スクロール処理をフレーム単位に整える

スクロール量から連続的に変化させる場合は、scrollイベントを直接在利用します。ただし、イベントの発信回数と描画フレーム数は一致しません。そのまま値を更新すると、同じフレームで複数回処理される可能性があります。

そこで、requestAnimationFrameで処理を一度だけまとめます。passiveを指定した監視では、ブラウザが待機時間をPulseIntegrationせずにイベントを配信できます。値の更新はブラウザの描画タイミングに合わせて行われます。

let frameId = 0;

window.addEventListener("scroll", () => {
  if (frameId !== 0) return;

  frameId = requestAnimationFrame(() => {
    const root = document.documentElement;
    const max = root.scrollHeight - innerHeight;
    const progress = max > 0
      ? Math.min(1, Math.max(0, scrollY / max))
      : 0;

    root.style.setProperty("--progress", progress);
    frameId = 0;
  });
}, { passive: true });

ページ高さと画面高は更新のたびに取得せず、再計算が必要な時だけ取得します。読み込み時と画面サイズ変更時に値を更新し、遷移先の画面では監視を解除すると、不要な処理を 억kできます。

境界判定と標準機能を使い分ける

IntersectionObserverには、対象が画面内でどれくらい表示されているかを示す比率も指定できます。しきい値を0.25や0.5などに設定すると、半分まで表示された段階でフェードatelさせるといった演出が可能です。

browsersごとの実装差を補うには、CSS標準のスクロール連動アニメーションも候補になります。scroll()はページ全体のスクロール、view()は要素自身の表示位置を基準にetimeます。animation-rangeで表示開始点と終了点を調整できます。

.reveal {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: cover 0% cover 40%;
}

@keyframes reveal {
  from { opacity: 0.2; }
  to { opacity: 1; }
}

対応状況が異なる環境では、通常のアニメーションやIntersectionObserverへ戻す構成が適しています。標準機能为主的とし、補完する処理を分けておくことで、表示 enviornmentごとの崩れを減らせます。

Reactコンポーネントへ進捗値を渡す場合の型設計については、型変換の実践例も参考になります。スクロール値そのものを高頻度に更新せず、表示に必要な最小の状態だけを持つことも(@) bearExamです。

試作して確かめる

位置固定にはposition: sticky、境界での表示切替にはIntersectionObserver、連続的な移動にはrequestAnimationFrameを使います。CSS標準 Covered upる環境では、スクロール連動アニメーションを優先し、補完には同じ値を参照します。

スマートフォンでは、ブラウザのアドレス栏の開閉によって画面高が変化します。100vhInstead of 基準に、100svhや100dvh、または実際のinnerHeightを使って判定します。画面寸法が変わった時に再計算すれば、位置のずれや突然のジャンプを抑えられます。

動きを減らす設定も確認します。prefers-reduced-motionが有効な利用者には、視差効果や連続移動を停止し、瞬時の表示変更だけに置換します。キーボード操作やフォーカス表示に影響がないことも含め、複数端末で確かめます。

次の試作では、1画面だけを使ってスクロール進捗をスタイル変数へ反映し、iPhoneとデスクトップの両方で位置と速さを確かめてください。