Toast — トースト通知(成功・エラー・情報・警告)
このコンポーネントについて
トースト通知(toast notification)は、画面の端に一時的にポップアップするメッセージUIです。「保存しました」「エラーが発生しました」のように操作結果を即座にユーザーへ伝える用途に適しており、モーダルと違って操作をブロックしないのが特徴です。
このページでは、成功・エラー・情報・警告の4種類のトーストを、右上から表示して3秒後に自動消去するパターンを紹介します。複数のトーストを同時に表示でき、手動での閉じるボタンにも対応しています。
- 4種類のタイプ — 成功(緑)・エラー(赤)・情報(青)・警告(黄)をアイコンと左ボーダーカラーで識別
- 自動消去(3秒) — プログレスバーが残り時間を視覚的に表示
- 手動クローズ — × ボタンでいつでも閉じられる
- 複数スタック — トーストは縦に積み重なり、それぞれ独立して動作
- スライドアニメーション — 右から滑り込む表示・消去アニメーション
実装のポイント・注意点
トーストの表示アニメーションは opacity: 0; transform: translateX(20px) から始めて、クラス付与後に opacity: 1; transform: translateX(0) へ遷移させています。ここで重要なのが requestAnimationFrame を2重に呼ぶ点です。要素をDOMに追加した直後にクラスを付与してもブラウザがレイアウトを計算しきれず、アニメーションが効かないことがあります。requestAnimationFrame の中でさらに requestAnimationFrame を呼ぶことで、確実に「次の描画フレーム以降」にクラスを付与できます。
setTimeout のタイマーIDは toast.dataset.timer に保存しています。手動で閉じる際は clearTimeout でタイマーをキャンセルしないと、既に削除された要素を再度削除しようとしてエラーになる場合があります。
トーストコンテナは pointer-events: none にして、コンテナ自体のクリックを透過させています。トースト本体に pointer-events: auto を戻すことで、トーストだけクリック可能になります。コンテナのクリック透過がないと、コンテナ背後のページコンテンツが操作できなくなります。
HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。
デモ
ボタンをクリックすると、画面右上にトーストが表示されます。3秒後に自動消去、または × ボタンで手動で閉じられます。
サンプルソース
3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js
— 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>トースト通知サンプル</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<!-- トースト表示コンテナ(右上に固定) -->
<div class="toast-container" id="toast-container"></div>
<!-- デモ用トリガーボタン -->
<div class="btn-group">
<button class="trigger-btn trigger-btn--success"
onclick="showToast('success', '保存しました', '変更内容が正常に保存されました。')">成功</button>
<button class="trigger-btn trigger-btn--error"
onclick="showToast('error', 'エラーが発生しました', '通信に失敗しました。再度お試しください。')">エラー</button>
<button class="trigger-btn trigger-btn--info"
onclick="showToast('info', 'お知らせ', '新しいバージョンが利用可能です。')">情報</button>
<button class="trigger-btn trigger-btn--warn"
onclick="showToast('warn', '注意', '未保存の変更があります。')">警告</button>
</div>
<script src="./script.js"></script>
</body>
</html>
:root { --toast-radius: 10px; }
*, *::before, *::after { box-sizing: border-box; }
body { font-family: sans-serif; padding: 24px; color: #1A2332; }
/* デモ用ボタン群 */
.btn-group { display: flex; flex-wrap: wrap; gap: 10px; }
.trigger-btn {
padding: 10px 22px; border: none; border-radius: 8px;
font-size: 14px; font-family: sans-serif; font-weight: 600;
cursor: pointer; transition: opacity 0.15s;
}
.trigger-btn:hover { opacity: 0.8; }
.trigger-btn--success { background: #DCFCE7; color: #166534; }
.trigger-btn--error { background: #FEE2E2; color: #991B1B; }
.trigger-btn--info { background: #DBEAFE; color: #1E40AF; }
.trigger-btn--warn { background: #FEF3C7; color: #92400E; }
/* ===== トーストコンテナ(右上固定) ===== */
.toast-container {
position: fixed;
top: 20px; right: 20px;
z-index: 9999;
display: flex; flex-direction: column; gap: 10px;
pointer-events: none; /* コンテナ自体はクリックを透過 */
max-width: 340px;
width: calc(100vw - 40px);
}
/* ===== トースト本体 ===== */
.toast {
display: flex; align-items: flex-start; gap: 12px;
padding: 13px 14px;
background: #fff;
border-radius: var(--toast-radius);
box-shadow: 0 4px 24px rgba(0,0,0,.12), 0 1px 4px rgba(0,0,0,.08);
border-left: 4px solid transparent;
pointer-events: auto; /* トースト自体はクリック可 */
position: relative; overflow: hidden;
/* 初期状態:非表示 */
opacity: 0; transform: translateX(20px);
transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast--show { opacity: 1; transform: translateX(0); }
.toast--hide { opacity: 0; transform: translateX(20px); }
.toast--success { border-left-color: #22C55E; }
.toast--error { border-left-color: #EF4444; }
.toast--info { border-left-color: #3B82F6; }
.toast--warn { border-left-color: #F59E0B; }
/* アイコン */
.toast-icon {
width: 24px; height: 24px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 13px; font-weight: 700; flex-shrink: 0;
}
.toast--success .toast-icon { background: #DCFCE7; color: #16A34A; }
.toast--error .toast-icon { background: #FEE2E2; color: #DC2626; }
.toast--info .toast-icon { background: #DBEAFE; color: #2563EB; }
.toast--warn .toast-icon { background: #FEF3C7; color: #D97706; }
/* テキスト */
.toast-body { flex: 1; min-width: 0; }
.toast-title {
font-size: 13px; font-weight: 700; color: #1A2332;
margin: 0 0 2px; line-height: 1.4;
}
.toast-msg { font-size: 12px; color: #5A6A7A; margin: 0; line-height: 1.5; }
/* 閉じるボタン */
.toast-close {
width: 20px; height: 20px; background: none; border: none;
font-size: 12px; color: #9AA5B4; cursor: pointer; padding: 0;
display: flex; align-items: center; justify-content: center;
border-radius: 4px; transition: color 0.15s, background 0.15s;
font-family: sans-serif;
}
.toast-close:hover { color: #5A6A7A; background: #F1F5F9; }
/* プログレスバー(3秒カウントダウン) */
.toast-progress {
position: absolute; bottom: 0; left: 0;
height: 3px;
animation: toast-countdown 3s linear forwards;
}
.toast--success .toast-progress { background: #22C55E; }
.toast--error .toast-progress { background: #EF4444; }
.toast--info .toast-progress { background: #3B82F6; }
.toast--warn .toast-progress { background: #F59E0B; }
@keyframes toast-countdown {
from { width: 100%; }
to { width: 0; }
}
var toastContainer = document.getElementById('toast-container');
// トーストを表示する
function showToast(type, title, message) {
var icons = { success: '✓', error: '✕', info: 'ℹ', warn: '⚠' };
var toast = document.createElement('div');
toast.className = 'toast toast--' + type;
toast.setAttribute('role', 'alert');
// アイコン
var icon = document.createElement('span');
icon.className = 'toast-icon';
icon.setAttribute('aria-hidden', 'true');
icon.textContent = icons[type];
// テキスト本体
var body = document.createElement('div');
body.className = 'toast-body';
var titleEl = document.createElement('p');
titleEl.className = 'toast-title';
titleEl.textContent = title;
var msgEl = document.createElement('p');
msgEl.className = 'toast-msg';
msgEl.textContent = message;
body.appendChild(titleEl);
body.appendChild(msgEl);
// 閉じるボタン
var closeBtn = document.createElement('button');
closeBtn.className = 'toast-close';
closeBtn.type = 'button';
closeBtn.setAttribute('aria-label', '閉じる');
closeBtn.textContent = '✕';
closeBtn.onclick = function() { dismissToast(toast); };
// プログレスバー
var progress = document.createElement('div');
progress.className = 'toast-progress';
toast.appendChild(icon);
toast.appendChild(body);
toast.appendChild(closeBtn);
toast.appendChild(progress);
toastContainer.appendChild(toast);
// 表示アニメーション(2重 rAF で確実にトランジションを効かせる)
requestAnimationFrame(function() {
requestAnimationFrame(function() {
toast.classList.add('toast--show');
});
});
// 3秒後に自動消去
var timer = setTimeout(function() { dismissToast(toast); }, 3000);
toast.dataset.timer = String(timer);
}
function dismissToast(toast) {
clearTimeout(Number(toast.dataset.timer));
toast.classList.remove('toast--show');
toast.classList.add('toast--hide');
toast.addEventListener('transitionend', function() {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, { once: true });
}
AI用プロンプト
以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。
ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。表示位置や自動消去の秒数変更など、要件を追記して使うのがおすすめです。
※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。
💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。
# トースト通知 作成依頼
## 概要
画面右上に一時的に表示され、3秒後に自動消去されるトースト通知UIを実装してください。
## 要件
- 成功・エラー・情報・警告の4種類のトーストを表示できる
- 各タイプをアイコン・左ボーダー・アイコン背景色で色分けする(成功:緑、エラー:赤、情報:青、警告:黄)
- 3秒後に自動消去する。残り時間はプログレスバーで視覚的に表示する
- × ボタンで手動でも閉じられる
- 複数のトーストを同時に表示でき、縦に積み重なる
- 右からスライドインして右にスライドアウトするアニメーションを付ける
- トーストコンテナは pointer-events: none にしてページ操作をブロックしない
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要(スマホでは横幅いっぱいに表示)
## 動作詳細
showToast(type, title, message) 関数を呼ぶとトーストを生成してコンテナに追加する。
トーストのタイプは 'success' / 'error' / 'info' / 'warn' を文字列で渡す。
表示アニメーションは requestAnimationFrame を2重に呼んで確実にトランジションを発火させる。
タイマーIDは dataset に保存し、手動クローズ時に clearTimeout でキャンセルする。
テキストの挿入はすべて textContent を使いXSS対策を徹底する。
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。