gsap

GSAP ScrollTriggerのpinの使い方|固定・ずれる原因・画像切り替えを解説

GSAP ScrollTriggerのpinで要素を固定する基本から、start・end・pinSpacing、画像切り替え、位置がずれる原因と直し方、レスポンシブ対応まで初心者向けに解説します。

この記事の目次
  1. ScrollTriggerのpinとは
  2. GSAPとScrollTriggerを読み込む
  3. npmで使う場合
  4. scriptタグで使う場合
  5. pinの基本的な使い方
  6. HTML
  7. CSS
  8. JavaScript
  9. triggerとは別の要素を固定する
  10. startとendで固定する範囲を決める
  11. pinSpacingで固定後の空白を調整する
  12. 画像を固定しながらスクロールで切り替える
  13. HTML
  14. CSS
  15. JavaScript
  16. ScrollTriggerのpinがずれる原因と直し方
  17. 1. markersでstartとendを確認する
  18. 2. 固定要素そのものをアニメーションしていないか確認する
  19. 3. 画像・Webフォント・開閉要素の読み込み後に再計算する
  20. 4. 親要素のtransformとwill-changeを確認する
  21. 5. pinSpacingと親レイアウトを確認する
  22. 6. ScrollTriggerをページ上部から順番に作る
  23. 7. pinを入れ子にしない
  24. 8. 高速スクロール時だけ一瞬ずれる場合
  25. スマホではpinを無効にする
  26. アクセシビリティとパフォーマンスの注意点
  27. pinが動かないときのチェックリスト
  28. よくある質問
  29. pin: trueとpin: “.element”の違いは何ですか?
  30. position: stickyではなくpinを使う理由は何ですか?
  31. pinSpacing: falseにすれば空白はすべて消えますか?
  32. スクロールに合わせてアニメーションも進めたい場合は?
  33. まとめ
  34. 関連記事
  35. 参考リンク

GSAPのScrollTriggerで要素を一定区間だけ固定したい場合は、pinを使います。

最小構成では、triggerに基準となる要素を指定し、pin: trueを設定するだけです。

gsap.registerPlugin(ScrollTrigger);

ScrollTrigger.create({
  trigger: ".pin-section",
  pin: true,
  start: "top top",
  end: "+=800",
  markers: true,
});

ただし、実際の制作では「固定位置がずれる」「固定後に大きな空白ができる」「スマホでレイアウトが崩れる」といった問題が起こりやすいです。

この記事では、ScrollTriggerのpinの基本から、startendpinSpacingの考え方、画像切り替えの実装、ずれる原因と直し方まで順番に解説します。

やりたいこと 主に使う設定
要素そのものを固定する pin: true
triggerとは別の要素を固定する pin: ".target"
固定開始・終了位置を変える startend
固定後の余白をなくす pinSpacing: false
位置を再計算する ScrollTrigger.refresh()
画面幅で固定を切り替える gsap.matchMedia()

ScrollTriggerのpinとは

pinは、スクロール位置がstartからendの範囲にある間、指定した要素を画面内に固定する機能です。

固定中もページ全体のスクロールは続くため、次のような表現に使えます。

  • 説明文をスクロールしながら、商品画像を横に固定する
  • 画面いっぱいのセクションを固定して、文字や画像を切り替える
  • 横スクロール風のアニメーション区間を作る
  • 長いストーリーの進行に合わせて、メインビジュアルを固定する

ScrollTriggerは固定時に、対象要素をpin-spacerというラッパーで囲みます。通常は固定していた距離に応じた余白も自動で追加されるため、固定解除後のコンテンツが急に詰まりません。

重要:ScrollTriggerが位置を正しく計算できるように、固定する要素自体を移動・拡大・縮小するアニメーションは避けます。動かしたい要素は、固定要素の内側に置いてアニメーションさせてください。

GSAPとScrollTriggerを読み込む

ScrollTriggerはGSAP本体とは別のプラグインです。利用前に読み込み、gsap.registerPlugin(ScrollTrigger)で登録します。

npmで使う場合

npm install gsap
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

プラグイン登録は、ビルド時のtree shakingでScrollTriggerが除外されることを防ぐ意味でも必要です。

scriptタグで使う場合

GSAP本体、ScrollTriggerの順番で読み込み、その後に自分のJavaScriptを実行します。

<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/ScrollTrigger.min.js"></script>
<script>
  gsap.registerPlugin(ScrollTrigger);
</script>

実運用では、動作確認したバージョンを固定して更新管理するのがおすすめです。

pinの基本的な使い方

まずは、ひとつのセクションを800px分のスクロール中だけ固定する例を見てみましょう。

HTML

<section class="pin-section">
  <div class="pin-section__inner">
    <p>このセクションが固定されます</p>
  </div>
</section>

CSS

.pin-section {
  min-height: 100vh;
}

.pin-section__inner {
  display: grid;
  min-height: 100vh;
  place-items: center;
  background: #e9edf2;
}

JavaScript

gsap.registerPlugin(ScrollTrigger);

ScrollTrigger.create({
  trigger: ".pin-section",
  pin: true,
  start: "top top",
  end: "+=800",
  markers: true,
});

pin: trueにすると、triggerに指定した.pin-sectionが固定対象になります。

最初はmarkers: trueを付けて、画面上に表示されるstart・endマーカーを見ながら調整すると理解しやすくなります。本番公開時はmarkersを削除するかfalseにします。

triggerとは別の要素を固定する

pinには、真偽値だけでなくセレクタやDOM要素も指定できます。

ScrollTrigger.create({
  trigger: ".story",
  pin: ".story__visual",
  start: "top top",
  end: "bottom bottom",
  markers: true,
});

この例では、.storyが固定区間を決める基準で、実際に固定されるのは.story__visualです。

設定 役割
trigger startとendを計算する基準要素
pin 実際に固定する要素

startとendで固定する範囲を決める

startendは、ScrollTriggerを理解するうえで最も重要な設定です。

ScrollTrigger.create({
  trigger: ".pin-section",
  pin: true,
  start: "top top",
  end: "bottom top",
});

start: "top top"は「triggerの上端が、画面の上端に重なったら開始」という意味です。

end: "bottom top"は「triggerの下端が、画面の上端に重なったら終了」という意味です。

固定距離を数値で決めたい場合は、開始位置からの相対値を使えます。

end: "+=1200"

これは「startから1200pxスクロールした位置で終了」という意味です。

  • "top center":要素の上端が画面中央に来たら開始
  • "top 80%":要素の上端が画面上から80%の位置に来たら開始
  • "+=100%":開始位置からスクローラー1画面分進んだら終了
  • "max":最大スクロール位置で終了

開始・終了位置を詳しく調整したい場合は、ScrollTriggerのstart・endとタイミングの調整方法も参考にしてください。

pinSpacingで固定後の空白を調整する

ScrollTriggerは通常、固定した距離に応じたpaddingをpin-spacerへ追加します。この余白があるため、後続コンテンツは固定要素の下へ自然につながります。

余白を追加したくない場合は、pinSpacing: falseを指定します。

ScrollTrigger.create({
  trigger: ".pin-section",
  pin: true,
  start: "top top",
  end: "+=800",
  pinSpacing: false,
});

ただし、無条件にfalseへ変えると、後続要素が固定中の要素の下へ入り込むことがあります。「空白があるから消す」のではなく、まずstartendと親要素の高さが正しいか確認してください。

親がdisplay: flexの場合はpaddingによる押し出しが期待どおりに働かないことがあり、ScrollTriggerではpinSpacingが既定でfalseになります。絶対配置された親でも、自分で余白を設計する必要があります。

画像を固定しながらスクロールで切り替える

画像切り替えでは、画像を表示する領域だけを固定し、説明文が画面中央へ入るたびに表示画像を変えます。

HTML

<section class="story">
  <div class="story__visual" aria-live="polite">
    <img src="image-01.jpg" alt="サービスの画面例">
  </div>

  <div class="story__steps">
    <section class="story__step" data-image="image-01.jpg" data-alt="サービスの画面例">
      <h2>最初の説明</h2>
      <p>最初の画像について説明します。</p>
    </section>
    <section class="story__step" data-image="image-02.jpg" data-alt="設定画面の例">
      <h2>次の説明</h2>
      <p>設定画面について説明します。</p>
    </section>
    <section class="story__step" data-image="image-03.jpg" data-alt="完成画面の例">
      <h2>完成</h2>
      <p>最後の状態を説明します。</p>
    </section>
  </div>
</section>

CSS

.story {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 48px;
}

.story__visual {
  height: 100vh;
}

.story__visual img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.story__step {
  display: grid;
  min-height: 100vh;
  align-content: center;
}

@media (max-width: 767px) {
  .story {
    grid-template-columns: 1fr;
  }

  .story__visual {
    height: auto;
  }

  .story__visual img {
    height: auto;
  }
}

JavaScript

gsap.registerPlugin(ScrollTrigger);

const mediaQuery = gsap.matchMedia();

mediaQuery.add("(min-width: 768px)", () => {
  const image = document.querySelector(".story__visual img");
  const steps = gsap.utils.toArray(".story__step");

  ScrollTrigger.create({
    trigger: ".story",
    pin: ".story__visual",
    start: "top top",
    end: "bottom bottom",
  });

  steps.forEach((step) => {
    ScrollTrigger.create({
      trigger: step,
      start: "top center",
      end: "bottom center",
      onEnter: () => changeImage(step),
      onEnterBack: () => changeImage(step),
    });
  });

  function changeImage(step) {
    image.src = step.dataset.image;
    image.alt = step.dataset.alt;
  }
});

onEnterは下方向へスクロールして説明文に入ったとき、onEnterBackは上方向へ戻ったときに実行されます。画像を単に差し替えるだけでなく、gsap.to()でフェードを加えることもできます。

補足:以前使われていたScrollTrigger.matchMedia()は、GSAP 3.11以降では非推奨です。現在は、作成したアニメーションやScrollTriggerを条件変更時にまとめて解除できるgsap.matchMedia()を使います。

ScrollTriggerのpinがずれる原因と直し方

pinの位置がずれるときは、数値を勘で変更するより、次の順番で確認すると原因を切り分けやすくなります。

1. markersでstartとendを確認する

まずmarkers: trueを追加し、ScrollTriggerが認識している開始位置と終了位置を見ます。

ScrollTrigger.create({
  trigger: ".pin-section",
  pin: true,
  start: "top top",
  end: "bottom top",
  markers: true,
});

マーカー自体が想定外の位置にある場合は、trigger、親要素の高さ、画像読み込み前後のレイアウトを確認します。

2. 固定要素そのものをアニメーションしていないか確認する

固定要素へxyscaleなどを適用すると、事前に計算した位置と見た目がずれる原因になります。

<div class="pin-target">
  <div class="animated-content">動かす内容</div>
</div>

.pin-targetを固定し、内側の.animated-contentを動かしてください。

3. 画像・Webフォント・開閉要素の読み込み後に再計算する

画像の高さが後から決まったり、アコーディオンを開いてページの高さが変わったりすると、作成時の計算結果が古くなります。

window.addEventListener("load", () => {
  ScrollTrigger.refresh();
});

DOMを変更した直後で描画を待ちたい場合は、ScrollTrigger.refresh(true)による安全な再計算も利用できます。なお、単なるスクロール位置の更新にはupdate()がありますが、要素の位置や高さが変わった場合に必要なのはrefresh()です。

4. 親要素のtransformとwill-changeを確認する

祖先要素にtransformwill-change: transformがあると、ブラウザのposition: fixedの基準が変わり、固定要素が跳ねたり一緒に動いたりすることがあります。

まずは、固定対象の祖先から不要なtransformwill-changeを外してください。

どうしても外せない場合は、最終手段としてpinReparent: trueがあります。

ScrollTrigger.create({
  trigger: ".pin-section",
  pin: true,
  pinReparent: true,
});

固定中の要素が一時的にbody直下へ移動するため、祖先に依存するCSSセレクタが効かなくなる可能性があります。DOM移動のコストもあるので、最初から使う設定ではありません。

5. pinSpacingと親レイアウトを確認する

余白が大きすぎるときは、次を確認します。

  • endで固定距離を長くしすぎていないか
  • 親要素が必要以上に高くなっていないか
  • display: flexposition: absoluteの中で余白を期待していないか
  • pinSpacing: falseにした結果、要素が重なっていないか

pin-spacerへ独自のclip-pathを設定する必要は通常ありません。生成されたラッパーを直接装飾するより、元のHTMLとCSSで見た目を作るほうが安定します。

6. ScrollTriggerをページ上部から順番に作る

上側のpinで追加される距離は、下側のScrollTriggerの開始位置にも影響します。そのため、基本的にはページ上部にあるものから順番に作成します。

後からScrollTriggerを追加した場合は、すべて作成したあとにScrollTrigger.refresh()を実行します。

7. pinを入れ子にしない

ScrollTriggerはpinの入れ子をサポートしていません。すでに固定される要素の内側で、さらに別の要素をpinしない構成にしてください。

固定される親要素の内側に通常のScrollTriggerを置く場合は、必要に応じてpinnedContainerを指定します。ただし、これは内側の要素も固定するための設定ではありません。

8. 高速スクロール時だけ一瞬ずれる場合

大きなセクションを高速でスクロールしたときだけ、固定前の状態が一瞬見えることがあります。その場合はanticipatePinを試せます。

ScrollTrigger.create({
  trigger: ".pin-section",
  pin: true,
  anticipatePin: 1,
});

多くの場合は既定値のままで問題ありません。レイアウトやstartの設定を確認したうえで、必要なときだけ使います。

スマホではpinを無効にする

画面が狭いスマホでは、固定要素が表示領域の大部分を占め、本文を読みづらくすることがあります。デスクトップだけpinを有効にし、スマホは通常の縦並びにする設計が安全です。

const mediaQuery = gsap.matchMedia();

mediaQuery.add("(min-width: 768px)", () => {
  ScrollTrigger.create({
    trigger: ".pin-section",
    pin: true,
    start: "top top",
    end: "+=800",
  });
});

// SPAなどで画面を破棄するときに実行
// mediaQuery.revert();

gsap.matchMedia()内で作成したアニメーションとScrollTriggerは、条件が一致しなくなったときに自動で元へ戻されます。ReactやVueなどで画面を破棄する場合は、コンポーネント側でも適切にクリーンアップしてください。

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

pinを使った表現は目を引きますが、スクロール距離を極端に長くすると、読み手が現在位置を把握しづらくなります。

  • 本文を読むために必要なスクロール量を増やしすぎない
  • スマホでは通常レイアウトも検討する
  • 画像にwidthheightaspect-ratioを設定し、読み込み時のずれを防ぐ
  • 画像切り替えでも内容を理解できる代替テキストを付ける
  • prefers-reduced-motionを考慮し、不要な動きを減らす

動きを減らす設定をpinにも反映する場合は、条件をまとめて判断できます。

const mediaQuery = gsap.matchMedia();

mediaQuery.add(
  {
    isDesktop: "(min-width: 768px)",
    reduceMotion: "(prefers-reduced-motion: reduce)",
  },
  (context) => {
    const { isDesktop, reduceMotion } = context.conditions;

    if (!isDesktop || reduceMotion) return;

    ScrollTrigger.create({
      trigger: ".pin-section",
      pin: true,
      start: "top top",
      end: "+=800",
    });
  }
);

pinが動かないときのチェックリスト

  1. GSAP本体とScrollTriggerの両方を読み込んでいるか
  2. gsap.registerPlugin(ScrollTrigger)を実行したか
  3. triggerpinのセレクタが存在するか
  4. markers: trueでstart・endを確認したか
  5. 固定要素そのものをtransformで動かしていないか
  6. 祖先に不要なtransformwill-changeがないか
  7. 画像やフォントの読み込み後に高さが変わっていないか
  8. endstartより後ろにあるか
  9. pinを入れ子にしていないか
  10. レスポンシブ条件の解除時に古いScrollTriggerが残っていないか

よくある質問

pin: trueとpin: “.element”の違いは何ですか?

pin: truetrigger要素そのものを固定します。pin: ".element"は、triggerとは別の要素を固定します。

position: stickyではなくpinを使う理由は何ですか?

単純な固定だけならCSSのposition: stickyで十分なことがあります。スクロール区間とアニメーションを同期したい、GSAPのコールバックやタイムラインと連携したい場合はScrollTriggerのpinが便利です。

pinSpacing: falseにすれば空白はすべて消えますか?

自動追加される余白はなくなりますが、後続要素が固定要素の下へ重なる可能性があります。先にendと親要素の高さを確認し、レイアウト上必要な場合だけ使ってください。

スクロールに合わせてアニメーションも進めたい場合は?

pinに加えてscrubを使います。実装方法はScrollTriggerのscrubでスクロール連動アニメーションを作る方法で解説しています。

まとめ

ScrollTriggerのpinは、triggerを基準に、startからendまで要素を固定する機能です。

まずは次の構成から始めてください。

gsap.registerPlugin(ScrollTrigger);

ScrollTrigger.create({
  trigger: ".pin-section",
  pin: true,
  start: "top top",
  end: "+=800",
  markers: true,
});

位置がずれたときは、markers、画像読み込み後のrefresh()、祖先のtransform、固定要素自体のアニメーション、pinSpacingの順に確認します。

レスポンシブ対応では、非推奨のScrollTrigger.matchMedia()ではなくgsap.matchMedia()を使い、スマホでpinが本当に必要かも含めて設計することが大切です。

関連記事

参考リンク