スクロールすると要素が奥行きを持って立体的に重なるアニメーション
おしゃれなサイトでよく見る、スクロール中に要素が奥から手前へ重なっていくような立体的なアニメーションの実装方法について解説しています。
本記事ではperspectiveやtranslateZは使わず、GSAPによるtranslateYとscaleの補間だけでこの演出を実現する方法を紹介します。
後続の要素が固定されてシンプルに重なる実装については下記をご覧ください。 >>スクロールすると要素が固定され後続の要素が重なるアニメーション
実装例
早速ですが下記が実装例です。
スクロールに応じてグレーのパネルが重なり、立体感を演出しています。単調なスクロールに一味違うアクセントをつけられるかもしれません。
実際にはリストになっているアイテムや画像をアニメーションさせるとより自然な演出となります。Lenisなどのスムーススクロールライブラリとの相性も良いでしょう。
解説
ここから実装の中身を見ていきます。HTMLは次のとおりです。
<ul class="list" data-wrapper> <li class="item" data-item></li> <li class="item item--02" data-item></li> <li class="item item--03" data-item></li></ul>ラッパーの要素にdata-wrapper属性を、その子要素にdata-item属性を設定しているシンプルなリスト要素です。
CSSは次のとおりです。
.list { margin: 0 auto; width: 80%; display: grid; row-gap: 100px;}
.item { width: 100%; height: 75vh; background-color: #ddd;}
.item--02 { background-color: #bbb;}
.item--03 { background-color: #aaa;}パネル要素である.itemに対して便宜上個別に高さを指定していますが、必須ではありません。高さは中の要素に依存する形でOKです。
JavaScriptでは次のコードを実行しています。事前にGSAP本体とScrollTriggerを読み込んでいます。
// 各アイテムの y / scale の補間の端点const ANIMATION_RANGE = { yPercentStart: 250, yPercentEnd: 30, scaleStart: 0.75, scaleEnd: 0.98,};
// 線形補間const lerp = (a, b, t) => { return a + (b - a) * t;};
// 0〜1 に正規化する(例: 3件なら index=0,1,2 → t=0, 0.5, 1)const indexToUnitProgress = (index, length) => { if (length <= 1) return 0; return index / (length - 1);};
const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => { const wrapper = document.querySelector("[data-wrapper]"); const items = gsap.utils.toArray("[data-item]"); // 「配列」として要素を取得
items.forEach((item, index) => { const t = indexToUnitProgress(index, items.length); const yPercent = lerp( ANIMATION_RANGE.yPercentStart, ANIMATION_RANGE.yPercentEnd, t, ); const scale = lerp(ANIMATION_RANGE.scaleStart, ANIMATION_RANGE.scaleEnd, t);
gsap.to(item, { y: `${yPercent}%`, scale, ease: "none", // イージング関数を指定しない(線形アニメーション=linear) scrollTrigger: { trigger: item, endTrigger: wrapper, start: "center center", end: "bottom center", scrub: true, }, }); });});立体感の演出
[data-item]属性の要素をy軸方向に移動させ、サイズを縮小しています。立体感を演出するために、移動距離と縮小率は要素ごとに変えています。
奥にある要素ほど小さく・上に位置するという人間の遠近感の知覚を利用しています。perspectiveやtranslateZを使わず、translateYとscaleの線形補間だけで擬似的にZ軸の奥行きを表現しています。
ScrollTriggerの設定
要素の中央がビューポートの中央に重なる時にアニメーションがスタートし、[data-wrapper]属性の要素の底部がビューポートの中央に重なる時にアニメーションが終了します。詳細は次の通りです。
| プロパティ | 設定値 | 意味 |
|---|---|---|
| start | center center | トリガー要素の中央がビューポートの上端から50%(=ビューポート中央)に来たとき |
| end | bottom center | endTrigger(wrapper)の底辺がビューポート中央に来たとき |
| scrub | true | スクロール量とアニメーション進行をリニアに同期 |
演出のカスタマイズ
今回の例では3つ目のアイテムが画面中央に来る頃には1つ目のアイテムは完全に見えなくなっていますが、y軸の移動距離と縮小率を調整することで3つのアイテムの重なりを画面内で表現することも可能です。
ANIMATION_RANGEの値を変更することで、要素の移動距離と縮小率を調整できます。それぞれ開始値と終了値を設定するようになっており、各要素にはインデックスに応じた線形補間の結果としてyPercentとscaleが割り当てられます。
自動的に補間値を計算するような仕組みになっているため、要素が増減する場合にも対応できます。
アクセシビリティ上の注意
今回のようなスクロールに応じて要素が重なる動きは、めまいを誘発する可能性があるアニメーションで、WCAGでもprefers-reduced-motion: reduceへの配慮が推奨されています。
実装例でもgsap.matchMedia()で分岐し、OSで「視差効果を減らす」が有効になっていない場合にのみアニメーションを実行するようにしています。prefers-reduced-motion: reduceが有効な場合には、アニメーションは行わず素のリストとして表示されます。
まとめ
translateY+scaleの線形補間だけで立体的な重なりを表現できるANIMATION_RANGEを調整すれば演出をカスタマイズできるprefers-reduced-motionへの配慮を行うことで、めまいを誘発しないアニメーションを実現できる
Webサイトにちょっとしたアクセントをつけたい時にサクッと実装できるモーションです。
