【JavaScript】トースト通知を実装する

javascript-icon JavaScript

こんにちは、かつコーチです。

前回はカウントアップアニメーションを扱いました。

今回は「保存しました」「エラーが発生しました」といった操作結果を、画面の隅にさりげなく表示するトースト通知を実装していきます。

alert()と違って処理を止めず、数秒で自動的に消えるのが特徴です。

トースト通知とは?

alertとの違い

alert()はブラウザ標準のダイアログですが、OKを押すまで画面全体の操作がブロックされてしまいます。

一方トースト通知は、画面の一部にメッセージを表示するだけで、他の操作を妨げません。

  • ユーザーの操作を止めない
  • 一定時間で自動的に消える
  • 複数のメッセージを積み重ねて表示できる

フォーム送信後の完了通知や、非同期処理のエラー表示など、「ユーザーに気づいてほしいけど、作業は止めたくない」場面に向いています。

実装に必要な要素

トースト通知の実装は、大きく3つの要素で成り立っています。

  • 通知を表示するための土台となるコンテナ
  • 通知本体を動的に生成してコンテナに追加する処理
  • 一定時間後に自動で消す処理(アニメーション込み)

これらを1つの関数にまとめて、どこからでもshowToast("メッセージ")のように呼び出せるようにするのがゴールです。

基本の実装手順

手順1:HTMLとCSSでトーストの土台を作る

まず、通知を積み重ねて表示するためのコンテナをbody直下に用意します。

<div id="toast-container"></div>

<button id="save-btn">保存する</button>
<button id="error-btn">エラーを起こす</button>
#toast-container {
  position: fixed;
  bottom: 20px;
  right: 20px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  z-index: 1000;
}
.toast {
  min-width: 240px;
  padding: 12px 16px;
  border-radius: 6px;
  color: #fff;
  font-size: 0.9rem;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  opacity: 0;
  transform: translateX(20px);
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.toast.show {
  opacity: 1;
  transform: translateX(0);
}
.toast--success { background: #16a34a; }
.toast--error { background: #dc2626; }
.toast--info { background: #2563eb; }

opacitytransformを初期状態でずらしておき、.showクラスが付いたときにアニメーションさせる構成です。

手順2:トーストを生成して表示するJavaScript

続いて、メッセージを受け取ってトーストを表示する関数を作ります。

function showToast(message, type = "info", duration = 3000) {
  const container = document.getElementById("toast-container");

  const toast = document.createElement("div");
  toast.className = `toast toast--${type}`;
  toast.textContent = message;
  container.appendChild(toast);

  // 追加直後にshowクラスを付けてアニメーションさせる
  requestAnimationFrame(() => {
    toast.classList.add("show");
  });

  setTimeout(() => {
    toast.classList.remove("show");
    toast.addEventListener("transitionend", () => toast.remove());
  }, duration);
}

createElementで作った直後にclassList.add("show")しても、ブラウザが最初のスタイルを描画する前だとアニメーションが省略されてしまうことがあります。

そのためrequestAnimationFrameで1フレーム待ってからshowクラスを付けることで、確実にトランジションを発火させています。

手順3:ボタンから呼び出す

最後に、実際のイベントとつなげます。

document.getElementById("save-btn").addEventListener("click", () => {
  showToast("保存しました", "success");
});

document.getElementById("error-btn").addEventListener("click", () => {
  showToast("保存に失敗しました。もう一度お試しください", "error", 4000);
});

これで、ボタンを押すたびに右下からトーストがスライドインして、数秒後にフェードアウトしながら消えます。

よくあるつまずきポイント・エラー対処

つまずき:連続クリックで前のトーストが消える前に次が出て要素が積み重なりすぎる

私が最初にこの機能を作ったとき、ボタンを連打されるテストを想定しておらず、トーストが画面に何十個も積み重なってしまうバグを出してしまいました。

❌ Before:削除処理が個別のsetTimeoutだけに依存している

function showToastBad(message) {
  const container = document.getElementById("toast-container");
  const toast = document.createElement("div");
  toast.className = "toast toast--info show";
  toast.textContent = message;
  container.appendChild(toast);

  setTimeout(() => {
    container.removeChild(toast); // 存在しない場合はここでエラーになることもある
  }, 3000);
}

連打すると同時に何個ものトーストが積まれ、画面が埋め尽くされてしまいました。

さらに、何らかの理由で先に要素が消えていた場合、removeChildが「対象のノードが見つからない」というエラーを投げることもありました。

✅ After:同時表示数に上限を設けて古いものから消す

const MAX_TOASTS = 3;

function showToastGood(message, type = "info", duration = 3000) {
  const container = document.getElementById("toast-container");

  // 上限を超えていたら、一番古いトーストを先に消す
  while (container.children.length >= MAX_TOASTS) {
    container.firstElementChild.remove();
  }

  const toast = document.createElement("div");
  toast.className = `toast toast--${type}`;
  toast.textContent = message;
  container.appendChild(toast);

  requestAnimationFrame(() => toast.classList.add("show"));

  setTimeout(() => {
    toast.classList.remove("show");
    toast.addEventListener("transitionend", () => toast.remove());
  }, duration);
}

container.children.lengthで現在の表示数をチェックし、上限を超えていたら一番古い要素(firstElementChild)を先に消すようにしました。

removeChildのような対象指定の削除ではなくelement.remove()を使うことで、「すでに消えている要素を消そうとしてエラーになる」問題も避けられます。

連打されても画面が埋まらなくなり、実際のプロダクト運用でも安心して使える実装になりました。

応用・一歩先の使い方

通知の種類ごとにアイコンを出し分ける

typeに応じてアイコン用の文字列を出し分けると、視認性がさらに上がります。

const ICONS = {
  success: "✅",
  error: "⚠️",
  info: "ℹ️",
};

function showToast(message, type = "info", duration = 3000) {
  const container = document.getElementById("toast-container");
  const toast = document.createElement("div");
  toast.className = `toast toast--${type}`;
  toast.textContent = `${ICONS[type] ?? ""} ${message}`;
  container.appendChild(toast);
  requestAnimationFrame(() => toast.classList.add("show"));
  setTimeout(() => {
    toast.classList.remove("show");
    toast.addEventListener("transitionend", () => toast.remove());
  }, duration);
}

??(Nullish合体演算子)で、想定外のtypeが来てもアイコンなしで表示だけは崩れないようにしているのもポイントです。

クリックで手動で閉じられるようにする

自動で消えるだけでなく、ユーザーがすぐに閉じたい場合に備えてクリックでも消せるようにしておくと親切です。

toast.addEventListener("click", () => {
  toast.classList.remove("show");
});

自動消去のタイマーと手動クリックの両方から同じ「消すロジック」を呼べるように、削除処理を別関数に切り出しておくとコードが整理しやすくなります。

まとめ

この記事のポイント

  • トースト通知は「操作を止めない」「自動で消える」点でalert()と役割が異なる
  • requestAnimationFrameで1フレーム待ってからクラスを付けると、トランジションが確実に発火する
  • 表示数に上限を設けないと、連打によってトーストが無限に積み重なってしまう
  • element.remove()を使うと、要素の存在チェックを省略してもエラーになりにくい

次に読むべき記事

次回は、要素をつかんで動かす「ドラッグ&ドロップ」の実装を扱います。

→ 次の記事:ドラッグ&ドロップを実装する

タイトルとURLをコピーしました