GSAPタイムラインで作る、ボタンバウンスアニメーション
ホバー時にボタンがスライムのように弾むアニメーションの実装方法について解説しています。複数ステップにわたる動きを細かく制御するため、GSAPのタイムラインを使用しています。
実装例
早速ですが下記が実装例です。ボタンにホバーするとアニメーションが再生されます。
scaleXとscaleYを組み合わせて横に潰れ・縦に伸びる動きを表現し、最後にelasticのeaseで弾むような余韻をつけています。CTAボタンなどに使うと、控えめながら印象に残るインタラクションになります。
解説
HTML、CSS、JavaScriptの処理を順を追って解説していきます。
HTMLとCSS
HTMLは次のとおりです。
<a href="#" class="c-button js-button">すべて見る</a>アニメーションの対象となるボタンにjs-buttonクラスを付与しています。CSSは見た目のスタイリングのみで、アニメーション自体はJavaScript側で制御しています。
.c-button { --color-button-bg: #2f7fb2; --color-button-text: #fff;
display: inline-block; font-weight: 700; font-size: 20px; color: var(--color-button-text); background-color: var(--color-button-bg); border-radius: 99rem; padding: 0.75em 2.5em;}JavaScript
JavaScriptのアニメーションに関するコードは以下のとおりです。
// ボタンのバウンスアニメーション本体const playBounceAnimation = (target, onCompleteCallback) => { const tl = gsap.timeline({ defaults: { transformOrigin: "center bottom" }, onComplete: onCompleteCallback, });
tl.to(target, { scaleX: 1.05, scaleY: 0.95, yPercent: 0, duration: 0.12, ease: "power2.in", }); tl.to(target, { scaleX: 0.95, scaleY: 1.05, yPercent: -25, duration: 0.2, ease: "power2.inOut", }); tl.to(target, { scaleX: 1, scaleY: 1, yPercent: 0, duration: 0.2, ease: "power2.inOut", }); tl.to( target, { scaleX: 1.15, scaleY: 0.85, duration: 0.05, ease: "power2.out" }, "-=0.05", ); tl.to(target, { scaleX: 1, scaleY: 1, duration: 0.4, ease: "elastic.out(1.2, 0.4)", });};
// ボタンのアニメーションを初期化const initButtonAnimations = () => { const buttons = document.querySelectorAll(".js-button");
buttons.forEach((button) => { if (!button) return; let isAnimating = false;
button.addEventListener("mouseenter", () => { if (isAnimating) return; isAnimating = true; playBounceAnimation(button, () => { isAnimating = false; }); }); });};
initButtonAnimations();一言でまとめると、ホバー時にボタンを横に潰して跳ね上がらせ、着地の瞬間にもう一度潰してからelasticで元の形に戻しています。transformOrigin: "center bottom"により、底辺を軸にした弾む動きになっています。
JavaScriptのポイント
タイムラインの組み立て方と、isAnimatingフラグの役割を解説します。
タイムラインアニメーションで動きを表現
このアニメーションは5つのステップで構成されています。各ステップでdurationとeaseにそれぞれ異なる値を設定することで、単調にならない有機的な動きを実現しています。
| ステップ | 動き | duration | ease |
|---|---|---|---|
| 1 | 横に広がり縦に潰れる(踏み込み) | 0.12s | power2.in |
| 2 | 縦に伸びて上に跳ねる | 0.2s | power2.inOut |
| 3 | 元のサイズに戻り着地 | 0.2s | power2.inOut |
| 4 | 着地の瞬間に再び潰れる | 0.05s | power2.out |
| 5 | elasticで元の形に復帰 | 0.4s | elastic.out(1.2, 0.4) |
4つ目のステップは"-=0.05"で3つ目と0.05秒重ねて開始しています。着地と同時に潰れる動きを作ることで、スライムのような粘り気のある印象になります。タイムラインのposition parameterを使うと、このような重なりを数値で直感的に指定できます。
フラグの意味
initButtonAnimations関数内のisAnimatingフラグには、次の2つの役割があります。
- 他のボタンに影響を与えない:
forEachのコールバック内でlet isAnimating = falseを宣言しているため、ボタンAとボタンBが同時にホバーされても、互いのフラグに影響しません。 - アニメーション実行中の多重発火をブロック:アニメーション中の再ホバーで
playBounceAnimationが重複実行されるのを防ぎます。onCompleteコールバックでフラグをfalseに戻すことで、次のホバーに備えています。
CSS実装との比較
似たような見た目のアニメーションはCSSのみでも実装可能です。ただし、以下の点でGSAPの方が有利です。
- アニメーション中の再発火制御:CSSでは再生中に再度ホバーした場合の挙動を細かく制御できません。
- ステップごとのease設定:各キーフレームに異なるeasingを割り当てるには
animation-timing-functionをキーフレームごとに指定する必要があり、調整に手間がかかります。 - キーフレームの重なり:
"-=0.05"のようなステップ間の重なりは、CSSではanimation-delayの負の値で再現できますが、全体のタイムラインを俯瞰しながら細かな調整をする必要があります。
GSAPとCSSのみを比較すると次の表のとおりです。
| 項目 | GSAP | CSSのみ |
|---|---|---|
| 見た目の再現 | ◎ | ○(elasticは近似) |
| 調整のしやすさ | ◎ | △(%とeaseを手計算) |
| JavaScript依存 | あり | なし |
| タイムラインの重なり | 簡単 | 可能だが面倒 |
| 再生制御 | 柔軟 | 限定的 |
JavaScriptを追加できるプロジェクトであれば、GSAPのタイムラインを使った方が意図した動きに素早くたどり着けると思います。一方、JavaScriptを使わずに済ませたい場合はCSSでの実装も選択肢になります。
