Webページを開いたときに、じわっとテキストが徐々にフェードインして表示されるアニメーションをなんとかCSSのみで表現できないか試してみました。
2023/1/24 : IntersectionObserver API を用いたスクロールして可視領域に入ったときにフェードイン表示するデモ2 に変更しました。
今回は、左から右、右から左、上から下、下から上の4パターンのCSSアニメーションを考えてみました。
実際のデモはこちら。
デモ1(CSSのみ)
See the Pen Text fading animation with simple CSS by digistate (@digistate) on CodePen.
このサンプルの大まかな構造は、まずテキストは文字色をなくし、background-image
で単色で透過していくグラデーション背景(linear-gradient
)を設定します。
background-size
でこのグラデーション背景を拡大し、background-clip: text
を指定することでテキスト部分をグラデーション背景でくり抜かれた状態にし、background-position
でグラデーション背景の位置を透過部分とテキストが重なるようにずらすことで、初期状態ではテキストが見えないようにしておきます。
最後に、animation
プロパティで background-position
にて背景の位置を戻すキーフレームを指定することで、ページ表示時にCSSアニメーションが実行されてグラデーションでくり抜かれたテキストが表示される仕組みです。
HTMLの構造
HTMLは至ってシンプル。
アニメーションさせるテキストには、fade-text
という共通セレクタを付けておきます。
さらに、アニメーションのパターン(表示方向)を決めるセレクタ(to__right
, to__left
, to__top
, to__bottom
)のいずれかを指定しておきます。
<div class="fade-text to__right">左から右へフェードインするテキスト</div>
あとはCSSでアニメーションのキーフレームを作成すれば動作しますが、アニメーション時間や各要素間のアニメーション開始の時差などをCSSで決め打ちでまとめてしまうと、汎用性が全くなくなってしまうため、CSS変数(カスタムプロパティ)を利用してインラインのstyle属性で要素ごとでアニメーション時間、表示遅延時間、イージングの種類は自由に指定できるようにしておきます。
<div class="fade-text to__right" style="--duration: 2.4s; --delay: 0.6s; --ease: ease-out"> 左から右へフェードインするテキスト </div>
上記の style="--duration: 2.4s; --delay: 0.6s; --ease: ease-out"
の部分がCSS変数です。
後述のCSS(var()
)でそれを受け取って各要素から指定された条件でアニメーションを実行します。
CSS
アニメーションさせるテキストに共通して付ける .fade-text
セレクタは以下のように定義しています。
.fade-text{ display: block; color: transparent; font-size: 30px; line-height: 1.4; margin-bottom: 40px; background-clip: text; -webkit-background-clip: text; background-size: 300% 300%; }
ポイントは、color:transparent
で文字色を透過状態にし、background-clip:text
でこの部分はテキストでくり抜きにして、background-size:300% 300%
で背景エリアを拡大しています。
次にアニメーションのパターン(表示方向)ごとのCSSをコーディングします。
例えば、テキストを左から右へフェードイン表示する場合のCSSは以下のようにしています。
.fade-text.to__right { background-image: linear-gradient(to right, #fff 30%, rgba(255, 255, 255, 0) 60%); background-position: left 100% center; transform: translate(-20px, 0); -webkit-animation: toRight var(--duration, 2.4s) var(--ease) var(--delay, 0.6s) forwards; animation: toRight var(--duration, 2.4s) var(--ease) var(--delay, 0.6s) forwards; }
この場合は、background-image: linear-gradient(to right, #fff 30%, rgba(255, 255, 255, 0) 60%)
で白のグラデーション背景にし、background-position: left 100% center
で左から100%の位置に移動させ、animation
プロパティで左から右にフェードインするキーフレームを指定しています。
animation
プロパティは、右側にフェードインするための toRight
というアニメーションキーフレーム(後述)を指定し、アニメーション時間(var(--duration)
)、イージング(var(--ease)
)、開始遅延(var(--delay)
) についてはCSS変数で要素から指定された値になるようにしています。
animation: toRight var(--duration, 2.4s) var(--ease) var(--delay, 0.6s) forwards;
animation
プロパティで指定されているキーフレームは、アニメーション終了状態のCSSをセットします。
@keyframes toRight { 100% { transform: translate(0, 0); background-position: left 0% center; } }
この要領で、さらに左から右、上から下、下から上への初期状態のCSSとキーフレームを追加すれば完成です。
デモ2(スクロールでフェードイン)
デモ1ではCSSのみでフェードインテキストを表現してみましたが、ちょこっとだけ JavaScript を追加すれば、スクロールして対象要素(テキスト)が可視領域に到達したときにフワッとフェードインで表示させることができます。
See the Pen Text fading animation with simple CSS by digistate (@digistate) on CodePen.
対象要素が可視領域に入ったかどうかを監視するには、IntersectionObserver というネイティブの API のみで簡単に実装することができます。
まず、フェードインで表示する対象要素(.fade-text
)をすべて取得します。
const textItems = document.querySelectorAll('.fade-text');
監視を行うための IntersectionObserver のオブジェクトを生成します。
第1引数にはコールバック関数(showElements
)、第2引数には監視対象が可視領域に到達したとみなす条件(options
変数) を指定しています。
const observer = new IntersectionObserver( showElements, options );
上記で指定している可視領域に入ったとみなす条件(options
)は、ここでは要素がページ上にどの程度表示されたかを指定する threshold
というプロパティで 1 を指定して要素が全て可視領域内に現れたときに条件を満たすと判断します。
threshold
を 0 にした場合は、要素の端がページ内に到達したときに条件を満たします。
const options = { rootMargin: '0px', threshold: 1.0, // [0-1] };
その他、オプション(指定可能な条件)の詳細についてはドキュメントを参照してください。
IntersectionObserver
のコールバックに指定している関数(showElements
)の引数(entries
)にはすべての対象要素が渡され、forEach
でそれぞれの要素について到達条件を満たしたか否かをチェックし、条件を満たした場合は .reveal
クラスを追加し、条件から外れた場合は .reveal
クラスを削除するという処理をしています。
現在の状態が条件をクリアしているかを確認するには、対象要素(entry
)に渡される isIntersecting
で確認できます。
const showElements = ( entries ) => { entries.forEach( entry => { if ( entry.isIntersecting ) { // 監視対象の条件を満たしたら .reveal を追加 entry.target.classList.add( 'reveal' ); } else { // 監視対象の条件から外れたら .reveal を削除 // ※アニメーションを繰り返さない場合はコメントアウト entry.target.classList.remove( 'reveal' ); } } ); }
これですべての準備が整ったので、あとは監視を開始します。
textItems.forEach( text => { observer.observe( text ); } );
ここまでの流れをまとめた JavaScript 全体のコードは以下になります。
( function(){ // 監視対象の要素を取得 const textItems = document.querySelectorAll('.fade-text'); // 監視対象の要素に対する処理 const showElements = ( entries ) => { entries.forEach( entry => { if ( entry.isIntersecting ) { // 監視対象の条件を満たしたら .reveal を追加 entry.target.classList.add( 'reveal' ); } else { // 監視対象の条件から外れたら .reveal を削除 // ※アニメーションを繰り返さない場合はコメントアウト entry.target.classList.remove( 'reveal' ); } } ); } // 監視対象が到達したとみなす条件 const options = { rootMargin: '0px', threshold: 1.0, // [0-1] }; // 監視の内容、条件 const observer = new IntersectionObserver( showElements, options ); // 対象要素すべてについて監視を開始 textItems.forEach( text => { observer.observe( text ); } ); } )();
デモ3(GSAPを利用した場合)
より複雑なアニメーションで表示をしたい場合は、例えば GSAP という JavaScript のアニメーションライブラリと、GSAP の ScrollTrigger というプラグインを利用することで、簡単に実現できます。
GSAP や ScrollTrigger については、以下の記事でとても分かりやすく解説されています。
ScrollTriggerのわかりやすい解説
GSAP は利便性が高くかつ軽量で WebGL、canvas、SVG も扱える非常に優れたライブラリですが、独自ライセンスであるため、商用利用には注意が必要です。
See the Pen Text fading animation using GSAP and ScrollTrigger by digistate (@digistate) on CodePen.
HTMLの構造
GSAP では、to()
や fromTo()
などのアニメーションの開始、終了状態をパラメータで渡すだけでアニメーションを実行してくれるメソッドがありますが、この中でアニメーションの終了状態(to()
)を各要素から受け取るために、各要素(div
)に data
属性で先述のキーフレームの内容と同じスタイルをセットしておきます。
<div class="fade-text to__right" data-to-x="0" data-to-bg-position="left 0% center"> 左から右へフェードインするテキスト </div>
上記の data-to-x="0" data-to-bg-position="left 0% center"
の部分が終了状態の transform
、background-position
の値になり、これを GSAP (JavaScript)側で受け取り、アニメーションを実行します。
同じように右から左、上から下、下から上にフェードインする要素を用意します。
JavaScriptの構造
GSAP の to()
メソッドを利用してアニメーションの終了状態をセットします。
gsap.to( elem, // 対象要素 '.fade-in' など対象要素のセレクタをテキストで指定もOK { ease: 'power1.out', // イージングのパターン duration: 2.4, // アニメーション時間(秒) autoAlpha: 0, // 非表示(opacity:0, visibility: hidden) x: 100, // 水平方向(右)への移動距離(px) -100 とすると左へ移動 y: 100, // 垂直方向(下)への移動距離(px) -100とする上へ移動 xPercent: 10, // 水平方向(右)への移動距離(%) -10 とすると左へ移動 xPercent: 10, // 垂直方向(下)への移動距離(%) -10とする上へ移動 scale: 1, // 要素の拡大率(1 : 等倍) backgroundColor: #ffffff, // 背景カラー delay: 0.6 // アニメーション開始遅延(秒) yoyo: true, // 往復リピートさせる repeat: 0, // リピート回数 -1 で無限 // アニメーション終了時のコールバック onComplete: function() { console.dir('finished'); }, } );
その他にも、CSS で指定するような形でたくさんのパラメータが利用できます。
そして、スクロールを契機としてアニメーションを実行させるための ScrollTrigger は以下のようにして利用します。
ScrollTrigger.create({ trigger: elem, // 対象要素 '.fade-in' など対象要素のセレクタをテキストで指定もOK start: 'top 80%', // 対象要素のどの位置が画面のどの位置にきたらアニメーションを実行するか onEnter: function() { animateTo(elem) }, // アニメーション(GSAP)の処理 onEnterBack: function() { animateTo(elem) }, // 再び表示されたときの処理 onLeave: function() { hide(elem) }, // 画面から見えなくなったときの処理 scrub: true, // スクロールに同期してアニメーションを行う pin: true, // アニメーション中に要素を固定 });
アニメーション要素は複数あるため、querySelectorAll
で取得したオブジェクトを配列化してループ処理し、その中で ScrollTrigger を実行します。
// .fade-text セレクタを持つ要素を取得 const el = document.querySelectorAll('.fade-text'); // 各要素について Array.from( el, function(e, i) { // ScrollTrigger を実行 ScrollTrigger.create({ trigger: e, start: 'bottom 80%' onEnter: function() { animateTo( e, i ) } }); });
アニメーションが実行される onEnter
プロパティで指定している自作の animateTo()
という関数内で、GSAP の to()
関数が実行され、実際のフェードインアニメーションが行われます。
function animateTo( e, i ){ gsap.to( e, { ease: 'power1.out', duration: 2.4, x: e.dataset.toX || 0, y: e.dataset.toY || 0, backgroundPosition: e.dataset.toBgPosition || 'center', } ); }
GSAP側では、各要素に指定されているdata属性を e.dataset.toX
、e.dataset.toY
で transform
の値を受け取り、e.dataset.toBgPosition
で終了状態の background-position
の値を受け取っています。
おわりに
CSSのみで表現するといいながら、結局GSAPまで使ってゴリゴリにJavaScriptと絡めたサンプルも紹介してしまいましたが、表示エリア内に入ったときに要素をフワッと表示する処理は IntersectionObserver API のみで簡単に実装できますし、GSAP などの外部ライブラリをうまく利用することで、より複雑なアニメーション表示でも簡単に実装できるようになります。
スクロールでフェードインするテキストは、提供中の DigiPress テーマ専用のブロックエディター拡張プラグイン「DigiPress Ex – Blocks」に新たなブロックとして追加したいと思います。