🎬

【GSAP入門】CSSの animation-delay 手計算から卒業する、実務で使えるGSAP基礎とScrollTrigger

に公開

はじめに

Astro.jsのプロジェクトで、GSAP(GreenSock Animation Platform)を使った演出を数多く実装する機会がありました。

たとえば次のような演出です。

  • 見出しや画像をふわっと表示させる登場演出
  • スクロールに合わせてコンテンツが順番に表示される演出(ScrollTrigger)

CSSでレイアウトやスタイリングは書いてきたものの、transitionや@keyframesを使ったアニメーション実装の経験はほとんどなく、JavaScriptのアニメーションライブラリも初めてでした。この記事は、同じように「CSSは書けるけど、アニメーションはガッツリやったことがない」という人に向けた内容です。

事前知識

本題に入る前に、記事中で比較に使う4つの用語を簡単におさらいしておきます。CSSのレイアウトやスタイリングは分かるけど、アニメーション系のプロパティはあまり触れてこなかった、という人向けの内容です。すでにご存知の方は読み飛ばしてください。

transition: 特定のプロパティが変化するときに、その変化を滑らかにするCSSプロパティです。ホバー時の色変化など、シンプルな2状態間のアニメーションによく使われます。

.box {
  transition: opacity 0.3s ease;
}
.box:hover {
  opacity: 0.5;
}

@keyframes: 複数の状態を経由する、より複雑なアニメーションを定義するための記法です。animation プロパティと組み合わせて使います。

@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.box {
  animation: fadeIn 1s ease forwards;
}

animation-delay: アニメーションの開始タイミングを遅らせるプロパティです。複数の要素を少しずつずらして表示したいときによく使いますが、要素が増えるたびに 0.1s、0.2s、0.3s…と手計算で値を割り振る必要があり、地味に面倒です。

.item:nth-child(2) { animation-delay: 0.1s; }
.item:nth-child(3) { animation-delay: 0.2s; }

element.style: JavaScriptから要素のインラインスタイルを直接書き換える方法です。

document.querySelector(".box").style.opacity = "0.5"

このように、書きますが、複数プロパティを滑らかに変化させたり、イージングを効かせたりするには自前で計算処理を書く必要があり、実用的とは言えません。

これらの「面倒さ」がGSAPを使うことで解消される、というのがこの記事のポイントです。実際に4つを組み合わせて作った、カードが順番に登場するアニメーションが以下です。

@keyframesでフェードイン、animation-delayで順番にずらし、transitionはボタンのホバーに、element.styleはJavaScriptからの再生ボタンに使っています。

GSAPとは

GSAPはJavaScriptで動かす高機能なアニメーションライブラリです。
Webflowによる買収を経て、現在はWebflow上だけでも10万以上のサイトがGSAPをカスタムコードで利用していると発表されています(Webflow公式ブログ)。
パフォーマンスの良さと表現力の高さから、賞を取るようなクリエイティブサイトでも定番の選択肢になっており、実際の採用事例はGSAP公式のShowcaseページで見ることができます。
CSSアニメーションと比べたときのGSAPの強みは、主に次の3点です。

  • タイミング制御がしやすい: 複数のアニメーションを順番に、あるいは重ねて実行できる
  • 動的な値を扱いやすい: APIのレスポンスやマウス座標など、実行時に決まる値をそのまま渡せる
  • 豊富なイージング: elastic や bounce など、CSSの cubic-bezier だけでは表現しにくい動きが標準で用意されている

2024年10月にWebflowがGreenSock社を買収し、2025年4月30日にコア機能を含むすべてのプラグインが無料化されました(Webflow公式アナウンス)。以前は一部のプラグインが有料でしたが、現在は商用利用も含めてほぼすべての機能を無料で利用できます。

導入方法

とりあえず試す

CDNから読み込むだけで、すぐに試せます。

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

実際のプロジェクトで使う

実務ではnpmでインストールして使うのが基本です。

npm install gsap

Astroコンポーネントで使う

Astroはコンポーネントをビルド時にサーバー側でHTMLへ変換する仕組みのため、GSAPのようにブラウザのDOMを直接操作するライブラリは、クライアント側で実行されるように書く必要があります。Astroコンポーネント内の<script>タグはデフォルトでクライアント向けにバンドルされるので、その中でGSAPをインポートして使います。

---
// Hero.astro
---
<section class="hero">
  <h1 class="hero-title">Welcome</h1>
</section>

<script>
  import gsap from "gsap";

  gsap.from(".hero-title", {
    opacity: 0,
    y: 30,
    duration: 0.8,
  });
</script>

ScrollTriggerのようなプラグインを使う場合は、個別にインポートしてから登録します。

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

gsap.registerPlugin(ScrollTrigger);

CSSとGSAPを書き比べてみる

例えば「5個の要素を、少しずつタイミングをずらしながらフェードイン表示させる」というアニメーションを比較してみます。

CSSの場合:

.item { opacity: 0; animation: fadeIn 0.6s ease forwards; }
.item:nth-child(1) { animation-delay: 0s; }
.item:nth-child(2) { animation-delay: 0.1s; }
.item:nth-child(3) { animation-delay: 0.2s; }
.item:nth-child(4) { animation-delay: 0.3s; }
.item:nth-child(5) { animation-delay: 0.4s; }

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}

GSAPの場合:

gsap.from(".item", {
  opacity: 0,
  y: 20,
  duration: 0.6,
  stagger: 0.1,
});

stagger: 0.1 の1行だけで、「0.1秒ずつずらして順番に実行する」という指定ができます。CSSだと要素の数だけnth-childでanimation-delayを書き並べる必要があり、要素数が変わるたびに書き直しが発生しますが、GSAPなら要素がいくつに増えても.itemというセレクタとstaggerの値はそのままで対応できます。@keyframesを別途定義する必要がなく、JavaScript側で完結するのも大きな違いです。

タイムラインで複数要素を制御する

GSAPの真骨頂は gsap.timeline() です。複数のアニメーションを「順番に」「重ねて」実行できます。

const tl = gsap.timeline();
tl.from(".title", { opacity: 0, y: 20, duration: 0.6 })
  .from(".subtitle", { opacity: 0, y: 20, duration: 0.6 }, "-=0.3")
  .from(".button", { opacity: 0, scale: 0.8, duration: 0.4 })

"-=0.3" を指定するだけで、前のアニメーションと0.3秒重ねて開始できます。CSSで同じことをやろうとすると animation-delay を毎回手計算する必要がありますが、GSAPなら直感的に書けます。要素を増やしたり順番を入れ替えたりするときも、タイムラインの記述を並べ替えるだけで済むのも地味に嬉しいポイントです。
実際に動かすと以下のようになります。

タイトルが表示され終わる前にサブタイトルが動き出し、その後ボタンが登場する、というタイミングの重なりがtimelineだけで表現できています。

つまずきやすいポイント

GSAPを触り始めたときによく引っかかるポイントを2つ紹介します。

to と from の違い

gsap.to() は「現在の状態から指定した値へ」変化させます。一方 gsap.from() は「指定した値から現在の状態へ」変化させます。フェードインのように「見えない状態から始めたい」場合は from を使うと、CSS側に初期スタイルを書かなくて済むので便利です。

ただし注意点もあります。fromはJavaScriptが読み込まれてから初めて初期状態(opacity: 0など)を適用するため、回線が遅い環境では「要素が一瞬表示されてから消え、そこからフェードインし直す」というチラつきが発生することがあります。これを避けたい場合は、CSS側でもopacity: 0のような初期状態をあらかじめ指定しておくと確実です。

duration の単位

CSSの transition はミリ秒指定(300ms)で書かれることが多いですが、GSAPの duration は秒単位です。duration: 300 と書いてしまうミスはよくあるので注意してください。

ScrollTriggerでスクロール連動アニメーションを作る

はじめにのセクションで触れた通り、実際の案件で一番手を動かしたのがこのScrollTriggerプラグインです。スクロールの進み具合に合わせて要素を表示したり動かしたりできる、GSAPの中でも特に人気のプラグインです。CDNやnpmで別途読み込んで、プラグイン登録をしてから使います。

<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>

基本の書き方は、通常のアニメーションにscrollTriggerというオプションを追加するだけです。

gsap.from(".box", {
  opacity: 0,
  y: 50,
  duration: 1,
  scrollTrigger: {
    trigger: ".box",
    start: "top 80%",
  },
});

start: "top 80%"は「.boxの上端がビューポートの80%の位置に来たら発火する」という意味です。ここからよく使うオプションを1つずつ見ていきます。

scrub: アニメーションをスクロール量に直接同期させる

scrubを指定すると、アニメーションが自動再生されるのではなく、スクロールの進み具合そのものに同期して進む・戻るようになります。

gsap.to(".box", {
  x: 300,
  scrollTrigger: {
    trigger: ".box",
    start: "top center",
    end: "bottom center",
    scrub: true,
  },
});

scrub: trueだとスクロール量にぴったり追従しますが、scrub: 1のように数値を渡すと、その秒数分だけ遅れて追従するようになり、少し滑らかな動きになります。案件では「スクロールに合わせて画像がじわっとズームする」演出でよく使いました。

スクロールバーの動きと要素の動きが同期している様子です。

pin: 要素をその場に固定する

pin: trueを指定すると、指定した範囲だけ要素をビューポートに固定できます。「固定された背景の上でテキストだけが切り替わる」ような演出でよく使う機能です。

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

end: "+=1000"は「開始位置から1000px分スクロールするまで固定する」という意味です。pinを使うと直後の要素のレイアウトがずれることがあるので、意図しない余白ができていないか確認しながら調整するのがポイントです。

もう一つ実務でよくハマるのが「位置計算のタイミング」です。ScrollTriggerは基本的にページ読み込み時点の高さをもとにstart/endを計算しますが、画像の読み込み完了や動的なコンテンツ追加によって後から高さが変わると、この計算がずれてしまいます。対策としては次の3つが定番です。

  • 画像にはwidth/heightをあらかじめ指定して、CLSを防ぐ
  • 高さが後から変わることが分かっている場合は、変わったタイミングでScrollTrigger.refresh()を呼んで再計算させる
  • レスポンシブ対応で画面幅ごとに値を変える場合は、invalidateOnRefresh: trueを指定しておく

start / end の指定方法

startとendは"要素の基準位置 ビューポートの基準位置"という形式で指定します。

start: "top 80%",   // 要素の上端が、画面の上から80%の位置に来たとき
start: "center center", // 要素の中央が、画面の中央に来たとき
end: "bottom top",  // 要素の下端が、画面の上端に来たとき

最初は感覚がつかみにくいですが、次に紹介するmarkersを有効にしながら試すと理解しやすくなります。

markers: 開発中の可視化に必須

markers: trueを指定すると、startとendの位置が画面上に線とラベルで表示され、今どのタイミングで発火するのかが一目で分かります。

scrollTrigger: {
  trigger: ".box",
  start: "top 80%",
  markers: true, // 本番公開前に必ず削除する
}

開発中は必ずと言っていいほどお世話になりますが、本番用のビルドに残したままだと画面に線が表示されてしまうので、公開前に消し忘れないよう注意が必要です。

toggleActions: スクロールの向きごとに挙動を変える

toggleActionsを使うと、「下にスクロールして入ってきたとき」「上にスクロールして戻ったとき」など、4つのタイミングそれぞれで再生・停止・巻き戻しの挙動を個別に指定できます。

scrollTrigger: {
  trigger: ".box",
  start: "top 80%",
  toggleActions: "play none none reverse",
}

これは「入ってきたら再生(play)、下方向を通過しても何もしない(none)、上にスクロールして再度入ってきても何もしない(none)、上に抜けたら巻き戻す(reverse)」という指定です。一度表示したら消えない演出にしたいのか、スクロールを戻すたびに再生し直したいのかによって、この4つの組み合わせを調整することになります。

まとめ

  • CSSでは面倒だった「順番に動かす」「重ねて動かす」がタイムラインで簡単に書ける
  • 動的な値をそのままアニメーションに渡せる
  • ScrollTriggerを使えば、スクロール連動の演出もscrub・pin・toggleActionsといったオプションの組み合わせで柔軟に作れる
  • 2025年以降はコア機能が無料なので気軽に試せる

CSSアニメーション自体の経験が浅くても、GSAPの学習コストは思ったより低いです。むしろanimation-delayの手計算やelement.styleの面倒さを知らないままgsap.to() / gsap.from()とタイムラインから入れるのは、ある意味ラッキーだったかもしれません。

株式会社アクトビ

Discussion