バニラJSとCSSでfadeIn・fadeOutを再現したい【jQueryなし】

※本記事のコードは参考用です。使用前にご自身で動作確認をお願いします。

はじめに

JavaScriptで要素をフェードイン・フェードアウトさせたいとき、簡単に実現できるのはjQueryfadeIn()fadeOut() ですよね。
しかし、個人的にはjQueryを使わずにバニラJS(純粋なJavaScript)だけで完結させたいという気持ちがあります。

今回は、requestAnimationFrametransitionend を使って、バニラJSだけでスムーズなフェードアニメーションを実現する方法をご紹介します。

よかったら参考にしてみてください。

完成形コード

ボタンを用意し、クリックで対象の要素をフェードイン・フェードアウトさせるサンプルです。
完成形コードをhtmlとcss、javascriptに分けて記載します。また、デモページはこちらです。

まずはhtmlですがこちらは要素を操作するボタンと、フェードさせたい要素をとりあえず用意します。

<button id="toggleBtn">表示 / 非表示</button>
<div id="targetBox">アニメーションするボックス</div>

次にcssです。基本は装飾になりますが、フェードのアニメーションはtransitionで表現することになります。

フェードさせたい要素に関してはdisplay:noneopacity: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です。

requestAnimationFrametransitionend を利用しますが詳しい説明は後半で行います。

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

フェードイン時、例えば以下のように displayopacity を同時に設定してしまうと、CSSの transition は効きません。
一見、これでアニメーションしそうですが、一瞬で表示されてしまいます。

box.style.display = 'block';
box.style.opacity = 1; // ← transitionが発火しない

なぜかというと、その理由は、ブラウザの描画の仕組みにあります。
JavaScriptが実行されている間、ブラウザはすぐに画面を更新しません。まずスタイルやレイアウトの変更を内部的に記録しておき、すべての処理が終わってからまとめて画面を描画します。

このため、次のような流れになります

  1. display: block を設定 → 要素は「表示状態」にはなるが、まだ画面には描画されていない
  2. 続けてすぐに opacity: 1 を設定すると、ブラウザは displayopacity の両方を同じタイミングで描画しようとする
  3. 結果として、ブラウザは「最初から opacity: 1 だった」とみなし、opacity: 0 → 1 の変化が検出されず、transition は発火しない

このような流れでブラウザは opacity: 0 → 1 の変化に気づかないため、アニメーション(transition)が発動しないというわけです。

ここで役立つのが requestAnimationFrame() です。これは「次の描画の直前に指定した処理を実行する」ブラウザの仕組みです。

window.requestAnimationFrame() メソッドは、ブラウザーにアニメーションを行いたいことを知らせ、指定した関数を呼び出して次の再描画の前にアニメーションを更新することを要求します。このメソッドは、再描画の前に呼び出されるコールバック 1 個を引数として取ります。

Window.requestAnimationFrame() - Web API | MDN

box.style.display = 'block';
requestAnimationFrame(() => {
  box.style.opacity = 1;
});

このように書くと、

  1. JavaScriptで display: block に設定(まだ画面には反映されていない)
    →ブラウザの「次の描画フレーム」にまとめて処理
  2. requestAnimationFrame() で処理を予約する
    →ブラウザは「次のフレームを描画する直前」に予約された関数を実行する
  3. 描画フレーム直前に opacity が変わるので、ブラウザは「0→1の変化」と認識できる
  4. その結果、同じ描画フレームで描画されるが「変化あり」として 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; //フラグを戻す
});
  1. opacity: 0 を設定(アニメーション開始)
  2. 指定した transition 時間が経過
  3. transitionend イベントが発火
  4. そのタイミングで display: none を設定することで、自然に非表示になる

アニメーションフラグ isAnimation の意味

複数回 transitionend が発火したり、ボタンの連打によって不要な処理が走るのを防ぐため、isAnimation というフラグを使っています。

これにより、opacityのアニメーション中にdisplay:noneが実行されることがなくなります。

if (!isAnimation) return;

さいごに

いかがでしたでしょうか。工夫すればフェードイン・フェードアウト以外のアニメーションでも役立つのではないでしょうか。また、今回はクリックで要素のフェードイン・フェードアウトを実行しましたが、スクロールなどと組み合わせるのも良いのではないでしょうか。

ぜひ参考にしていただけると嬉しいです。

参考

Window.requestAnimationFrame() - Web API | MDN
requestAnimationFrame の使い方(アニメーション)
Element: transitionend イベント - Web API | MDN