gsap

GSAP ScrollTriggerのstart・end設定|発火タイミングを調整する方法

GSAP ScrollTriggerのstart・endで発火タイミングを調整する方法を解説します。top・center・bottom、%・px・相対値、markers、endTrigger、関数値、clamp、refresh、ずれる原因まで実装例で確認できます。

この記事の目次
  1. ScrollTriggerを準備する
  2. start・endの2つの値を読む
  3. 使えるkeyword
  4. 通常時のdefault
  5. よく使うstart・end設定
  6. percentage・pixel・相対値で調整する
  7. +=と-=で基準位置からずらす
  8. endをstartからの距離で指定する
  9. markersで発火位置を確認する
  10. 計算後のstart・endをpixelで確認する
  11. endTriggerで別要素を終了基準にする
  12. functionでresponsiveな位置を返す
  13. animation値もfunctionにする
  14. clampでpage範囲外の位置を防ぐ
  15. toggleActionsとcallbackが動く4つの境界
  16. 画像・font・DOM変更後はrefreshする
  17. refreshとupdateの違い
  18. custom scrollerを使う
  19. 横scrollのstart・end
  20. pinとstart・endの関係
  21. prefers-reduced-motionに対応する
  22. 発火タイミングがずれる原因
  23. IntersectionObserverのthreshold・rootMarginとは別
  24. transformする要素をtriggerにしている
  25. 古いScrollTriggerが残っている
  26. start・end設定のチェックリスト
  27. まとめ

GSAP ScrollTriggerの発火タイミングは、主にstartendで指定します。基本は「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側ともにtopcenterbottomを使えます。

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のstartendは、計算後の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が想定と違う場合は、次の順番で確認します。

  1. markers: trueidを付ける
  2. 第1値がtrigger側、第2値がscroller側か読み直す
  3. 意図したelementがtriggerに選ばれているか確認する
  4. windowとcustom scrollerを取り違えていないか確認する
  5. 画像・font・async contentで上部の高さが変わっていないか確認する
  6. DOM変更後にScrollTrigger.refresh()する
  7. 上部のpinより先に下部のScrollTriggerを作っていないか確認する
  8. trigger自体を大きくtranslateするanimationで測定基準を動かしていないか確認する
  9. 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の区間も意図どおりに調整できます。