【CSS】スクロールに応じて背景画像を切り替えるパララックス効果の実装方法

【CSS】スクロールに応じて背景画像を切り替えるパララックス効果の実装方法 HTML/CSS

背景画像をスクロールに応じて切り替える「パララックス効果」は、ユーザー体験を大きく向上させる演出のひとつです。従来のbackground-attachment: fixedを使った方法はモバイルSafariでの不具合やパフォーマンス低下が課題となるため、実運用ではCSS変数とIntersectionObserverを組み合わせ、軽量なtransformアニメーションで擬似的なパララックスを再現するのが有効です。

スクロール連動で背景を切り替えるパララックスの全体像

背景を固定したまま前景だけを動かす従来のparallaxはモバイルSafariでの不具合やパフォーマンス低下が課題でした。ここではCSS変数とIntersectionObserverを併用し、セクション交差に応じて背景画像をスムーズに切り替える方式と、transformを使ったGPUフレンドリーな擬似パララックスを組み合わせて、実運用しやすい実装を提示します。

最小構成(HTML)

<header class="kv" data-bg="/images/bg-hero.jpg">
  <h1>Parallax Background Switch</h1>
</header>

<section class="panel" data-bg="/images/bg-01.jpg">
  <h2>セクション1</h2>
  <p>本文…</p>
</section>

<section class="panel" data-bg="/images/bg-02.jpg">
  <h2>セクション2</h2>
  <p>本文…</p>
</section>

<section class="panel" data-bg="/images/bg-03.jpg">
  <h2>セクション3</h2>
  <p>本文…</p>
</section>

土台CSS(背景レイヤと変数制御)

/* ルートに現在の背景URLを保持するCSS変数 */
:root {
  --bg-url: url('/images/bg-hero.jpg');
  --bg-opacity: 1;
  --parallax-scale: 1.05;
  --parallax-shift: 0px;
}

/* 固定の背景レイヤ */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background-image: var(--bg-url);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  opacity: var(--bg-opacity);
  transform: translate3d(0, var(--parallax-shift), 0) scale(var(--parallax-scale));
  will-change: transform, opacity, background-image;
  transition:
    background-image 0s step-end,
    opacity .5s ease;
}

/* 前景 */
.kv, .panel {
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(24px, 5vw, 80px);
  color: #fff;
  text-shadow: 0 2px 8px rgba(0,0,0,.35);
  backdrop-filter: saturate(1.1) contrast(1.05);
}

.panel {
  background: linear-gradient(transparent, rgba(0,0,0,.12));
}

@media (prefers-reduced-motion: reduce) {
  body::before {
    transition: none;
    transform: none;
  }
}

IntersectionObserverで背景を切り替えるJavaScript

<script>
(() => {
  const sections = document.querySelectorAll('.kv, .panel');
  const setBg = (url) => {
    const img = new Image();
    img.src = url;
    document.documentElement.style.setProperty('--bg-opacity', '0');
    img.decode?.().catch(() => {})
      .finally(() => {
        document.documentElement.style.setProperty('--bg-url', `url('${url}')`);
        requestAnimationFrame(() => {
          document.documentElement.style.setProperty('--bg-opacity', '1');
        });
      });
  };

  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return;
      const target = entry.target;
      const bg = target.getAttribute('data-bg');
      if (bg) setBg(bg);
    });
  }, {
    root: null,
    rootMargin: '-30% 0px -30% 0px',
    threshold: 0
  });

  sections.forEach((sec) => observer.observe(sec));

  let ticking = false;
  const maxShift = 60;
  const onScroll = () => {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => {
      const y = window.scrollY || 0;
      const shift = (y * 0.08);
      const clamped = Math.max(-maxShift, Math.min(maxShift, shift));
      document.documentElement.style.setProperty('--parallax-shift', clamped + 'px');
      ticking = false;
    });
  };

  const media = window.matchMedia('(prefers-reduced-motion: reduce)');
  if (!media.matches) {
    window.addEventListener('scroll', onScroll, { passive: true });
  }

  const firstBg = sections[0]?.getAttribute('data-bg');
  if (firstBg) setBg(firstBg);
})();
</script>

背景画像のプリロード最適化

<link rel="preload" as="image" href="/images/bg-hero.jpg" imagesrcset="/images/bg-hero.jpg 1x, /images/bg-hero@2x.jpg 2x" imagesizes="100vw">
<img src="/images/bg-01.jpg" fetchpriority="low" decoding="async" alt="" style="position:absolute; width:0; height:0; opacity:0;">
<img src="/images/bg-02.jpg" fetchpriority="low" decoding="async" alt="" style="position:absolute; width:0; height:0; opacity:0;">
<img src="/images/bg-03.jpg" fetchpriority="low" decoding="async" alt="" style="position:absolute; width:0; height:0; opacity:0;">

モバイルSafari対策と実運用のコツ

iOS Safariではbackground-attachment: fixedによる古典的パララックスがカクつきや描画破綻の原因になりやすいため固定背景は使わず擬似的にtransformで動きを付けます。背景の切り替えではopacityトランジションだけをアニメーション対象にし、background-imageの変更はtransition対象から外すことで不要なレイアウト計算を避けます。テキスト可読性を守るためのテキストシャドウや前景の薄いグラデーションはコントラストを損ねない範囲で最小限に留め、色の下地が薄い背景に切り替わる区間でも読みやすさを維持します。

アクセシビリティとパフォーマンス

動きに敏感なユーザーのためにprefers-reduced-motionを尊重し、スクロールシフトやフェードを停止します。JavaScript側はscrollイベントをrequestAnimationFrameで間引き、スタイルの更新はCSS変数経由に限定してレイアウトスラッシングを避け、will-changeでGPU合成レイヤを事前に確保してフレーム落ちを防止します。画像はWebPやAVIFを優先しつつ、フォールバックを用意し、解像度はデバイスとレイアウトに適したものを提供します。

トラブルシューティング

切り替え時に白フラッシュが出る場合は画像のプリロードが不十分かopacity遷移が早すぎる可能性があります。transition-durationを0.5s以上にしてdecode完了後に差し替える流れを維持します。背景がぼやける場合は拡大率を抑え、–parallax-scaleを1.02前後に調整します。スクロール時にスタッターが見える場合はmaxShiftを小さくし、係数やrootMarginを見直して切り替え頻度を下げます。

まとめ

固定背景に依存せずCSS変数とIntersectionObserverで現在セクションを検知して背景URLを差し替え、transformによる軽量な擬似パララックスで奥行きを演出することで、モバイルでも滑らかに動く背景切り替えが実現できます。reduced motion対応とプリロード最適化を併用すれば可用性と表示速度を両立でき、記事やLPの世界観を崩さずに視覚的な引き込み効果を付与できます。