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」に新たなブロックとして追加したいと思います。

