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の基本から、start・end・pinSpacingの考え方、画像切り替えの実装、ずれる原因と直し方まで順番に解説します。
| やりたいこと | 主に使う設定 |
|---|---|
| 要素そのものを固定する | pin: true |
| triggerとは別の要素を固定する | pin: ".target" |
| 固定開始・終了位置を変える | start、end |
| 固定後の余白をなくす | 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で固定する範囲を決める
startとendは、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へ変えると、後続要素が固定中の要素の下へ入り込むことがあります。「空白があるから消す」のではなく、まずstart・endと親要素の高さが正しいか確認してください。
親が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. 固定要素そのものをアニメーションしていないか確認する
固定要素へx、y、scaleなどを適用すると、事前に計算した位置と見た目がずれる原因になります。
<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を確認する
祖先要素にtransformやwill-change: transformがあると、ブラウザのposition: fixedの基準が変わり、固定要素が跳ねたり一緒に動いたりすることがあります。
まずは、固定対象の祖先から不要なtransformとwill-changeを外してください。
どうしても外せない場合は、最終手段としてpinReparent: trueがあります。
ScrollTrigger.create({
trigger: ".pin-section",
pin: true,
pinReparent: true,
});
固定中の要素が一時的にbody直下へ移動するため、祖先に依存するCSSセレクタが効かなくなる可能性があります。DOM移動のコストもあるので、最初から使う設定ではありません。
5. pinSpacingと親レイアウトを確認する
余白が大きすぎるときは、次を確認します。
endで固定距離を長くしすぎていないか- 親要素が必要以上に高くなっていないか
display: flexやposition: 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を使った表現は目を引きますが、スクロール距離を極端に長くすると、読み手が現在位置を把握しづらくなります。
- 本文を読むために必要なスクロール量を増やしすぎない
- スマホでは通常レイアウトも検討する
- 画像に
width・heightやaspect-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が動かないときのチェックリスト
- GSAP本体とScrollTriggerの両方を読み込んでいるか
gsap.registerPlugin(ScrollTrigger)を実行したかtriggerとpinのセレクタが存在するかmarkers: trueでstart・endを確認したか- 固定要素そのものをtransformで動かしていないか
- 祖先に不要な
transformやwill-changeがないか - 画像やフォントの読み込み後に高さが変わっていないか
endがstartより後ろにあるか- pinを入れ子にしていないか
- レスポンシブ条件の解除時に古いScrollTriggerが残っていないか
よくある質問
pin: trueとpin: “.element”の違いは何ですか?
pin: trueはtrigger要素そのものを固定します。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が本当に必要かも含めて設計することが大切です。
関連記事
- GSAP ScrollTriggerのscrubでスクロール連動アニメーションを作る方法
- GSAP ScrollTriggerのstart・endとタイミングの調整方法
- GSAPのコールバックでアニメーションのタイミングを制御する方法
- GSAP Tweenの基本的な使い方