スクロールでふわっと表示されるアニメーション【Intersection Observer コピペ実装】

スクロールするとふわっと要素が現れる演出は、多くのWebサイトで使われています。昔は scroll イベントで実装していましたが、現在は Intersection Observer API を使うのがスタンダードです。パフォーマンスが良く、コードもシンプルです。

最小構成のコード(これだけでOK)

HTML

<div class="fade-in">ここがふわっと現れます</div>
<div class="fade-in">ここも</div>
<div class="fade-in">これも</div>

CSS

/* 初期状態:非表示 */
.fade-in {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

/* JS で追加されるクラス:表示状態 */
.fade-in.is-visible {
  opacity: 1;
  transform: translateY(0);
}

JavaScript

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
    }
  });
});

document.querySelectorAll('.fade-in').forEach((el) => {
  observer.observe(el);
});

これだけで動きます。外部ライブラリ不要、jQuery不要です。

仕組みの解説

Intersection Observer は「要素が画面内に入ったかどうか」を監視するAPIです。スクロールするたびにすべての要素をチェックする従来の方法と違い、変化があったときだけ処理が走る ので効率的です。

  • new IntersectionObserver(callback) でオブザーバーを作成
  • observer.observe(el) で監視対象を登録
  • 要素が画面に入ると entry.isIntersectingtrue になる
  • classList.add('is-visible') でアニメーション用クラスを付与

カスタマイズパターン

一度表示したら再度スクロールしても消さない(推奨)

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
      observer.unobserve(entry.target); // ← 表示後は監視を解除
    }
  });
});
カスタマイズポイント
unobserve を追加すると表示後は監視を止めるので、スクロールを戻しても消えません。UX的にはこちらの方が自然です。逆に消えてほしい場合は unobserve を外し、isIntersectingfalse のときに classList.remove('is-visible') を追加します。

下からではなく横からスライドイン

/* 左からスライドイン */
.slide-in-left {
  opacity: 0;
  transform: translateX(-40px);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.slide-in-left.is-visible {
  opacity: 1;
  transform: translateX(0);
}

/* 右からスライドイン */
.slide-in-right {
  opacity: 0;
  transform: translateX(40px);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.slide-in-right.is-visible {
  opacity: 1;
  transform: translateX(0);
}

要素ごとに時間差をつける(staggered)

/* CSS で data-delay 属性を使う */
.fade-in[data-delay="1"] { transition-delay: 0.1s; }
.fade-in[data-delay="2"] { transition-delay: 0.2s; }
.fade-in[data-delay="3"] { transition-delay: 0.3s; }
<!-- HTML -->
<div class="fade-in" data-delay="1">先に出る</div>
<div class="fade-in" data-delay="2">少し遅れる</div>
<div class="fade-in" data-delay="3">さらに遅れる</div>

画面の少し手前でトリガーしたい(rootMargin)

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add('is-visible');
      }
    });
  },
  {
    rootMargin: '0px 0px -80px 0px', // 画面下端から80px手前でトリガー
  }
);
カスタマイズポイント
rootMargin の第3値(下マージン)をマイナスにすると、要素が画面に完全に入る前にトリガーできます。-100px など値を大きくするほど手前で発火します。

threshold(何割見えたらトリガーするか)

const observer = new IntersectionObserver(
  (entries) => { /* ... */ },
  {
    threshold: 0.3, // 30%見えたらトリガー(0〜1で指定)
  }
);

アニメーションの速さ・距離の調整

/* ゆっくり・大きく動く */
.fade-in {
  opacity: 0;
  transform: translateY(50px); /* 距離を大きく */
  transition: opacity 1s ease-out, transform 1s ease-out; /* 時間を長く */
}

/* 素早く・小さく動く */
.fade-in {
  opacity: 0;
  transform: translateY(10px); /* 距離を小さく */
  transition: opacity 0.3s ease-out, transform 0.3s ease-out; /* 時間を短く */
}

モーション低減の設定への対応

アクセシビリティを考慮し、OSのアニメーション低減設定に対応しておくのがおすすめです。

@media (prefers-reduced-motion: reduce) {
  .fade-in {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

この設定をしておくと、アニメーションが苦手なユーザーにも安心なサイトになります。