バニラJSとCSSでfadeIn・fadeOutを再現したい【jQueryなし】
※本記事のコードは参考用です。使用前にご自身で動作確認をお願いします。
はじめに
JavaScriptで要素をフェードイン・フェードアウトさせたいとき、簡単に実現できるのはjQuery
の fadeIn()
や fadeOut()
ですよね。
しかし、個人的にはjQueryを使わずにバニラJS(純粋なJavaScript)だけで完結させたいという気持ちがあります。
今回は、requestAnimationFrame
と transitionend
を使って、バニラJSだけでスムーズなフェードアニメーションを実現する方法をご紹介します。
よかったら参考にしてみてください。
完成形コード
ボタンを用意し、クリックで対象の要素をフェードイン・フェードアウトさせるサンプルです。
完成形コードをhtmlとcss、javascriptに分けて記載します。また、デモページはこちらです。
まずはhtmlですがこちらは要素を操作するボタンと、フェードさせたい要素をとりあえず用意します。
<button id="toggleBtn">表示 / 非表示</button>
<div id="targetBox">アニメーションするボックス</div>
次にcssです。基本は装飾になりますが、フェードのアニメーションはtransition
で表現することになります。
フェードさせたい要素に関してはdisplay:none
とopacity:0
で最初は非表示にしてください。また、アニメーションのためのtransition
の設定をopacity
にかけることを忘れないようにしましょう。
button {
padding: 8px 20px;
border: 2px solid #333;
background-color: #fff;
border-radius: 8px;
}
.div {
margin-top: 20px;
padding: 20px;
display: none; //必須
width: 200px;
height: 100px;
background-color: skyblue;
text-align: center;
opacity: 0; //必須
transition : opacity 0.3s ease; //必須
}
最後に肝心のJSです。
requestAnimationFrame
と transitionend
を利用しますが詳しい説明は後半で行います。
window.addEventListener('DOMContentLoaded', function () {
const btn = document.getElementById('toggleBtn');
const box = document.getElementById('targetBox');
let isVisible = false;
let isAnimation = false;
btn.addEventListener('click', () => {
if (!isVisible) {
// フェードイン
box.style.display = 'block';
requestAnimationFrame(() => {
box.style.opacity = 1;
});
} else {
// フェードアウト
isAnimation = true; // フラグをtrueに設定
box.style.opacity = '0';
addEventListener("transitionend", (event) => {
if (!isAnimation) return; //アニメーション中はキャンセル
box.style.display = 'none';
isAnimation = false; //フラグを戻す
});
}
isVisible = !isVisible; //フラグを反転させる
});
})
JSについて
フェードイン:requestAnimationFrame
フェードイン時、例えば以下のように display
と opacity
を同時に設定してしまうと、CSSの transition
は効きません。
一見、これでアニメーションしそうですが、一瞬で表示されてしまいます。
box.style.display = 'block';
box.style.opacity = 1; // ← transitionが発火しない
なぜかというと、その理由は、ブラウザの描画の仕組みにあります。
JavaScriptが実行されている間、ブラウザはすぐに画面を更新しません。まずスタイルやレイアウトの変更を内部的に記録しておき、すべての処理が終わってからまとめて画面を描画します。
このため、次のような流れになります
display: block
を設定 → 要素は「表示状態」にはなるが、まだ画面には描画されていない- 続けてすぐに
opacity: 1
を設定すると、ブラウザはdisplay
とopacity
の両方を同じタイミングで描画しようとする - 結果として、ブラウザは「最初から
opacity: 1
だった」とみなし、opacity: 0 → 1
の変化が検出されず、transition
は発火しない
このような流れでブラウザは opacity: 0 → 1
の変化に気づかないため、アニメーション(transition)が発動しないというわけです。
ここで役立つのが requestAnimationFrame()
です。これは「次の描画の直前に指定した処理を実行する」ブラウザの仕組みです。
window.requestAnimationFrame()
メソッドは、ブラウザーにアニメーションを行いたいことを知らせ、指定した関数を呼び出して次の再描画の前にアニメーションを更新することを要求します。このメソッドは、再描画の前に呼び出されるコールバック 1 個を引数として取ります。
box.style.display = 'block';
requestAnimationFrame(() => {
box.style.opacity = 1;
});
このように書くと、
- JavaScriptで
display: block
に設定(まだ画面には反映されていない)
→ブラウザの「次の描画フレーム」にまとめて処理 requestAnimationFrame()
で処理を予約する
→ブラウザは「次のフレームを描画する直前」に予約された関数を実行する- 描画フレーム直前に opacity が変わるので、ブラウザは「0→1の変化」と認識できる
- その結果、同じ描画フレームで描画されるが「変化あり」として transition が発火する
このように、display: block
にしてもブラウザはすぐには描画を反映せず、次のフレームの描画直前に実行される requestAnimationFrame
のコールバック内で opacity: 1
を設定することで、ブラウザが「opacity が 0 から 1 に変化した」と認識し、transition
が発火します。
つまり、実行タイミングをずらすことでアニメーションを可能にしています。
また、requestAnimationFrame()
についてもっと詳しい知りたい方はrequestAnimationFrame の使い方(アニメーション)がおすすめです。
フェードアウト:transitionend
フェードアウトの際、単に opacity: 0
にしただけでは、見た目は消えますが実際には要素はDOMに残っています。(透明になっただけで要素は存在したままになってしまいます。)
そこで、完全に非表示にするためには display: none
に戻す必要があります。ただしすぐに display: none
を設定してしまうと、アニメーションが完了する前に要素が消えてしまいます。
そのため、アニメーションが終わったタイミングを検知する transitionend
イベントを使います。そうすることで、opacity
のアニメーションが終わったあとにdisplay:none
を実行することができます。
transitionend
は、CSSの transition
アニメーションが完了したときに自動で呼び出されるイベントです。
// フェードアウト
isAnimation = true; // フラグをtrueに設定
box.style.opacity = '0';
addEventListener("transitionend", (event) => {
if (!isAnimation) return; //アニメーション中はキャンセル
box.style.display = 'none';
isAnimation = false; //フラグを戻す
});
opacity: 0
を設定(アニメーション開始)- 指定した
transition
時間が経過 transitionend
イベントが発火- そのタイミングで
display: none
を設定することで、自然に非表示になる
アニメーションフラグ isAnimation
の意味
複数回 transitionend
が発火したり、ボタンの連打によって不要な処理が走るのを防ぐため、isAnimation
というフラグを使っています。
これにより、opacity
のアニメーション中にdisplay:none
が実行されることがなくなります。
if (!isAnimation) return;
さいごに
いかがでしたでしょうか。工夫すればフェードイン・フェードアウト以外のアニメーションでも役立つのではないでしょうか。また、今回はクリックで要素のフェードイン・フェードアウトを実行しましたが、スクロールなどと組み合わせるのも良いのではないでしょうか。
ぜひ参考にしていただけると嬉しいです。
参考
Window.requestAnimationFrame() - Web API | MDN
requestAnimationFrame の使い方(アニメーション)
Element: transitionend イベント - Web API | MDN