GSAP ScrollTriggerの発火タイミングは、主にstartとendで指定します。基本は「trigger要素のどの位置」と「viewportのどの位置」が重なったら開始・終了するかを、2つの値で表します。
最もよく使う例は次のとおりです。
gsap.to(".box", {
x: 300,
scrollTrigger: {
trigger: ".box",
start: "top 80%",
end: "bottom 20%",
markers: true,
},
});
この例は、boxの上端がviewport上端から80%の位置へ到達したときに開始し、boxの下端がviewport上端から20%の位置へ到達したときに終了します。
この記事では、keywords・percentage・pixel・相対値、markers、endTrigger、関数値、refreshまで順番に解説します。
ScrollTriggerを準備する
npmでGSAPをinstallし、ScrollTriggerをregisterします。
npm install gsap
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
基本のTweenを作ります。
gsap.from(".box", {
y: 80,
autoAlpha: 0,
duration: 0.8,
scrollTrigger: {
trigger: ".box",
start: "top 80%",
markers: true,
},
});
ScrollTriggerは、scroll eventのたびにelementの位置を常時測定する設計ではありません。作成時やrefresh時にstart / endをscroll位置の数値へ計算し、scroll中は現在位置と比較します。
start・endの2つの値を読む
文字列"top 80%"は、空白で分かれた2つの位置からできています。
| 位置 | 対象 | "top 80%"での意味 |
|---|---|---|
| 第1値 | trigger要素 | triggerの上端 |
| 第2値 | scroller。通常はviewport | viewport上端から80%の位置 |
つまり「triggerのtopがviewportの80%位置へ来たとき」です。startならactive区間へ入り、endならactive区間を出る境界になります。
使えるkeyword
縦scrollでは、trigger側・viewport側ともにtop、center、bottomを使えます。
start: "top bottom"
start: "top center"
start: "center center"
end: "bottom top"
keywordはpercentageでも表せます。viewport側のtopは0%、centerは50%、bottomは100%です。
通常時のdefault
通常のtriggerではstartのdefaultは"top bottom"、endのdefaultは"bottom top"です。ただし、pinを使うScrollTriggerではstartのdefaultが"top top"になります。
意図をコードから読み取れるように、調整が必要な箇所ではdefault任せにせず明記するのがおすすめです。
よく使うstart・end設定
目的別の設定を整理します。
| 設定 | 意味 | 用途 |
|---|---|---|
start: "top bottom" |
上端がviewport下端へ到達 | 画面へ入る直前 |
start: "top 80%" |
上端がviewportの80%位置へ到達 | 少し見えてから発火 |
start: "center center" |
要素中央とviewport中央が一致 | 中央を基準に演出 |
end: "bottom top" |
下端がviewport上端へ到達 | 完全に通過するまでactive |
end: "+=800" |
startから800px scrollした位置 | scrub・pin区間の長さ |
percentage・pixel・相対値で調整する
keywordsだけでなくpercentageやpixelを組み合わせられます。
scrollTrigger: {
trigger: ".box",
start: "top 75%",
end: "bottom 120px",
}
"bottom 120px"は、triggerのbottomがscroller上端から120pxの位置へ来たときです。
+=と-=で基準位置からずらす
各位置へ相対offsetを追加できます。
start: "top center+=100"
end: "bottom top-=50"
center+=100はviewport中央から下へ100px、top-=50はviewport上端より上へ50pxずらした位置です。markerを表示し、符号と移動方向を実画面で確認します。
endをstartからの距離で指定する
end: "+=1000"は、計算されたstartからさらに1,000px scrollした位置をendにします。
scrollTrigger: {
trigger: ".scene",
start: "top top",
end: "+=1000",
scrub: true,
}
これは「1,000pxの高さを要素へ付ける」という意味ではありません。ScrollTriggerのactive区間をscroll距離で定義します。scrubの進み方はScrollTriggerのscrub設定で詳しく解説しています。
markersで発火位置を確認する
開発中はmarkers: trueを付けると、start・endとscroller側の境界を視覚化できます。
scrollTrigger: {
id: "hero-box",
trigger: ".box",
start: "top 80%",
end: "bottom 20%",
markers: true,
}
複数のScrollTriggerがある場合はidを付けるとmarkerを区別しやすくなります。色・文字size・indentも変更できます。
markers: {
startColor: "#22c55e",
endColor: "#ef4444",
fontSize: "14px",
indent: 12,
}
markerはdebug用です。本番ではfalseにするか設定を削除します。
計算後のstart・endをpixelで確認する
ScrollTrigger instanceのstartとendは、計算後のscroll位置をpixel数で保持します。
const tween = gsap.to(".box", {
x: 300,
scrollTrigger: {
id: "box-motion",
trigger: ".box",
start: "top 80%",
end: "bottom 20%",
onRefresh(self) {
console.log({ start: self.start, end: self.end });
},
},
});
console.log(tween.scrollTrigger.start);
文字列設定と計算後の数値を分けて考えると、responsive時のずれを追いやすくなります。
endTriggerで別要素を終了基準にする
通常はtrigger要素がstartとendの両方の基準です。別要素をendの基準にしたい場合はendTriggerを使います。
ScrollTrigger.create({
trigger: ".chapter-start",
start: "top center",
endTrigger: ".chapter-end",
end: "bottom center",
markers: true,
onToggle(self) {
document.body.classList.toggle("is-reading-chapter", self.isActive);
},
});
chapter-startのtopがviewport中央へ来たときにactiveになり、chapter-endのbottomがviewport中央へ来たときにinactiveになります。
functionでresponsiveな位置を返す
start / endにはfunctionを渡せます。functionは位置を計算するたび、通常は作成時とrefresh時に呼ばれます。
const header = document.querySelector(".site-header");
gsap.to(".box", {
x: 300,
scrollTrigger: {
trigger: ".box",
start: () => `top top+=${header.offsetHeight}`,
end: () => `+=${window.innerHeight * 0.8}`,
invalidateOnRefresh: true,
markers: true,
},
});
functionを使えば、headerの実寸やviewport高に応じて位置を返せます。invalidateOnRefresh: trueは、関連animationが記録した開始値もrefresh時に破棄して取り直します。start / endのfunction自体はrefresh時に再評価されます。
animation値もfunctionにする
responsiveに移動量を変える場合は、Tween側もfunction valueにします。
gsap.to(".box", {
x: () => window.innerWidth * 0.25,
scrollTrigger: {
trigger: ".box",
start: "top 80%",
end: "+=500",
invalidateOnRefresh: true,
},
});
clampでpage範囲外の位置を防ぐ
page先頭付近のtriggerでは、計算されたstartが0未満になる場合があります。GSAP 3.12以降では文字列をclamp()で包み、0から最大scroll位置の範囲へ収められます。
scrollTrigger: {
trigger: ".hero",
start: "clamp(top bottom)",
end: "clamp(bottom top)",
}
first view内ですでに一部進んだscrub animationになる問題や、page末尾より先へendが出る問題の対策になります。
toggleActionsとcallbackが動く4つの境界
startとendには、進行方向・逆方向を合わせて4つの境界があります。
| 境界 | callback | 意味 |
|---|---|---|
| 前進してstart通過 | onEnter |
active区間へ入る |
| 前進してend通過 | onLeave |
active区間を出る |
| 逆方向でend通過 | onEnterBack |
下側からactive区間へ戻る |
| 逆方向でstart通過 | onLeaveBack |
上側へactive区間を出る |
toggleActionsはこの順番で4actionを指定します。
toggleActions: "play pause resume reset"
使えるactionはplay、pause、resume、restart、reverse、complete、reset、noneです。callbackの詳細はGSAPのcallback一覧で解説しています。
画像・font・DOM変更後はrefreshする
ScrollTriggerはlayoutを基にstart / endを計算します。作成後に上部の高さが変わると、markerと実際の見た目がずれる場合があります。
代表例は次のとおりです。
- 寸法を予約していない画像が読み込まれた
- Web fontで改行・要素高が変わった
- accordionやmodal以外の展開UIが通常flowを押し下げた
- 非同期dataを取得してsectionを追加した
- route変更後に新しいpage layoutへ切り替わった
DOMの高さが確定したあと、全ScrollTriggerを再計算します。
await document.fonts.ready;
ScrollTrigger.refresh();
clickで要素を展開した直後など、browserのlayout反映を少し待たせたい場合はsafe refreshを使えます。
accordionButton.addEventListener("click", () => {
panel.hidden = !panel.hidden;
ScrollTrigger.refresh(true);
});
refresh(true)は少なくとも1回のrequestAnimationFrame tickを待ち、最大およそ200msの範囲で安全にrefreshします。
refreshとupdateの違い
ScrollTrigger.refresh()はlayoutからstart / endを再計算します。ScrollTrigger.update()は現在のscroll位置を確認してprogress・animation・callbackを更新します。
位置がずれた問題にupdateだけを呼んでも、古いstart / endは再計算されません。DOM寸法が変わったらrefreshを使います。
custom scrollerを使う
windowではなくoverflow containerをscrollする場合はscrollerを指定します。
.scroll-area {
height: 70vh;
overflow-y: auto;
}
gsap.to(".box", {
x: 240,
scrollTrigger: {
scroller: ".scroll-area",
trigger: ".box",
start: "top 80%",
end: "bottom 20%",
markers: true,
},
});
この場合、第2値の80%や20%はwindowではなく.scroll-areaの表示領域を基準にします。smooth scroll libraryを組み合わせる場合は、そのlibraryとScrollTriggerの公式integration方法を確認します。
横scrollのstart・end
横方向をscroll基準にする場合はhorizontal: trueを指定し、left・center・rightを使います。
ScrollTrigger.create({
horizontal: true,
scroller: ".horizontal-scroller",
trigger: ".card",
start: "left 80%",
end: "right 20%",
markers: true,
});
縦scrollでsectionを横へ移動させる演出は、scrollbar自体は縦のままなのでhorizontal: trueとは別です。pinとscrubを使う横移動animationとして設計します。
pinとstart・endの関係
pinではstartからendまでのactive区間が固定時間になります。endが短ければすぐ解除され、長ければ固定区間が伸びます。
ScrollTrigger.create({
trigger: ".panel",
start: "top top",
end: "+=1200",
pin: true,
markers: true,
});
上にあるpinはpageのscroll距離を増やし、下のScrollTriggerの位置へ影響します。基本はpage上部から下部の順にScrollTriggerを作成します。固定のずれやpinSpacingはScrollTriggerのpin設定で詳しく扱っています。
prefers-reduced-motionに対応する
大きな移動やscroll連動は、動きを減らす設定の利用者へ負担になる場合があります。GSAPのmatchMediaで必要なanimationだけ作成します。
const media = gsap.matchMedia();
media.add("(prefers-reduced-motion: no-preference)", () => {
gsap.from(".box", {
y: 80,
autoAlpha: 0,
scrollTrigger: {
trigger: ".box",
start: "top 80%",
},
});
});
// page破棄時
media.revert();
情報を見せること自体をScrollTriggerへ依存させず、animationが無効でもcontentへ到達できる構造にします。
発火タイミングがずれる原因
start / endが想定と違う場合は、次の順番で確認します。
markers: trueとidを付ける- 第1値がtrigger側、第2値がscroller側か読み直す
- 意図したelementがtriggerに選ばれているか確認する
- windowとcustom scrollerを取り違えていないか確認する
- 画像・font・async contentで上部の高さが変わっていないか確認する
- DOM変更後に
ScrollTrigger.refresh()する - 上部のpinより先に下部のScrollTriggerを作っていないか確認する
- trigger自体を大きくtranslateするanimationで測定基準を動かしていないか確認する
- component・route破棄時に古いScrollTriggerが残っていないか確認する
IntersectionObserverのthreshold・rootMarginとは別
ScrollTriggerの2値をIntersectionObserverのthresholdとrootMarginへそのまま置き換えることはできません。ScrollTriggerはIntersectionObserverを内部利用せず、start / endをscroll位置へ計算してanimation progress、pin、scrubなどを制御します。
似た「画面へ入ったら処理する」用途でも、APIの意味は分けて理解します。
transformする要素をtriggerにしている
trigger要素自体をfrom animationで大きく移動すると、測定した位置とanimation開始前の見た目が混同しやすくなります。外側wrapperをtriggerにし、内側要素を動かす構造にすると安定します。
gsap.from(".box__inner", {
y: 100,
autoAlpha: 0,
scrollTrigger: {
trigger: ".box",
start: "top 80%",
},
});
古いScrollTriggerが残っている
SPAやcomponentでpageを切り替える場合は、作成範囲をcontextへまとめて破棄します。
const context = gsap.context(() => {
gsap.from(".box", {
y: 80,
scrollTrigger: {
trigger: ".box",
start: "top 80%",
},
});
}, containerElement);
// component unmount時
context.revert();
start・end設定のチェックリスト
- 第1値はtrigger、第2値はscrollerの位置として読む
- まず
start: "top 80%"とmarkersで基準を確認する - scroll距離を決めるなら
end: "+=数値"を使う - 別elementで終了するならendTriggerを指定する
- responsiveな実寸はfunction valueで返す
- animation値を取り直すならinvalidateOnRefreshを使う
- DOM寸法が変わったらrefresh、位置確認だけならupdateと使い分ける
- page端ではclampを検討する
- pinを含む場合は上から下の順に作成する
- 本番ではmarkersを外し、reduced motionとcleanupを確認する
まとめ
ScrollTriggerのstart / endは、「trigger側の位置」と「scroller側の位置」が一致するscroll座標を定義します。"top 80%"なら、triggerの上端がviewportの80%位置へ来たときです。
keyword、percentage、pixel、相対offset、startからの距離を使い分け、開発中はmarkersで確認します。画像・font・DOM追加でlayoutが変わったらScrollTrigger.refresh()を実行し、responsiveな値はfunctionとinvalidateOnRefreshで取り直します。
start / endを正しく設計すると、通常の発火animationだけでなく、scrubやpinの区間も意図どおりに調整できます。