トースト通知
成功・警告など種類別の通知を右下にスタック表示し、残り時間バーとともに自動消滅。操作後のフィードバック表示に使えます。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk:ダッシュボード操作のフィードバックをトーストで表示 -->
<div class="app">
<header class="app__bar">
<span class="app__logo">💼 FlowDesk</span>
<span class="app__user">田中 健 ▾</span>
</header>
<main class="app__main">
<h1 class="app__h1">プロジェクト設定</h1>
<p class="app__lead">変更を加えると、画面右下に結果を通知します。</p>
<div class="actions">
<button class="btn" data-toast="success">変更を保存</button>
<button class="btn" data-toast="info">メンバーを招待</button>
<button class="btn" data-toast="warning">同期を再試行</button>
<button class="btn" data-toast="error">プロジェクトを削除</button>
</div>
</main>
<!-- トーストはここにスタックされる -->
<div class="toasts" data-toasts aria-live="polite"></div>
</div>
CSS
/* FlowDesk SaaS テーマ */
:root{--navy:#0f1b34;--blue:#4f7cff;--ink:#1d2740;--line:#e3e8f2;--muted:#6b7794;--bg:#f4f6fb}
*{box-sizing:border-box}
body{margin:0;min-height:100vh;font-family:"Segoe UI",system-ui,sans-serif;background:var(--bg);color:var(--ink)}
.app{max-width:560px;margin:0 auto;min-height:100vh;display:flex;flex-direction:column}
.app__bar{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;background:var(--navy);color:#fff}
.app__logo{font-weight:700}
.app__user{font-size:.82rem;color:#aab6d6}
.app__main{flex:1;padding:24px 22px}
.app__h1{margin:0 0 4px;font-size:1.35rem;color:var(--navy)}
.app__lead{margin:0 0 20px;font-size:.86rem;color:var(--muted)}
.actions{display:grid;grid-template-columns:1fr 1fr;gap:12px;max-width:360px}
.btn{appearance:none;cursor:pointer;font:inherit;font-weight:600;font-size:.86rem;padding:12px;border-radius:10px;background:#fff;border:1px solid var(--line);color:var(--ink);transition:border-color .2s,transform .1s}
.btn:hover{border-color:var(--blue)}
.btn:active{transform:scale(.98)}
/* トースト:右下にスタックし残り時間バーで自動消滅 */
.toasts{position:fixed;right:18px;bottom:18px;display:flex;flex-direction:column;gap:10px;z-index:9;width:280px;max-width:calc(100vw - 36px)}
.toast{
position:relative;overflow:hidden;background:#fff;border-radius:12px;padding:13px 14px 14px 44px;
box-shadow:0 12px 30px rgba(15,27,52,.18);border-left:4px solid var(--blue);
animation:slide-in .3s cubic-bezier(.34,1.4,.6,1);
}
.toast.is-out{animation:slide-out .3s ease forwards}
.toast__icon{position:absolute;left:14px;top:13px;width:20px;height:20px;border-radius:50%;display:grid;place-items:center;color:#fff;font-size:.72rem;font-weight:800}
.toast__title{margin:0;font-size:.85rem;font-weight:700}
.toast__msg{margin:2px 0 0;font-size:.76rem;color:var(--muted);line-height:1.5}
.toast__bar{position:absolute;left:0;bottom:0;height:3px;background:currentColor;opacity:.5;animation:shrink var(--life,3500ms) linear forwards}
.toast.success{border-color:#22b07d;color:#22b07d}
.toast.info{border-color:var(--blue);color:var(--blue)}
.toast.warning{border-color:#e0a92a;color:#e0a92a}
.toast.error{border-color:#e2574c;color:#e2574c}
.toast.success .toast__icon{background:#22b07d}
.toast.info .toast__icon{background:var(--blue)}
.toast.warning .toast__icon{background:#e0a92a}
.toast.error .toast__icon{background:#e2574c}
.toast__title,.toast__msg{color:var(--ink)}
@keyframes slide-in{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
@keyframes slide-out{to{opacity:0;transform:translateX(40px)}}
@keyframes shrink{from{width:100%}to{width:0}}
@media (prefers-reduced-motion:reduce){.toast,.toast.is-out,.toast__bar{animation:none}}
JavaScript
// 種類別トーストを右下にスタック表示し、一定時間で自動消滅
const stack = document.querySelector('[data-toasts]');
const LIFE = 3500;
// 種類ごとの文言とアイコン
const PRESET = {
success: { icon: '✓', title: '保存しました', msg: 'プロジェクト設定を更新しました。' },
info: { icon: 'i', title: '招待を送信', msg: '田中さんへ招待メールを送りました。' },
warning: { icon: '!', title: '同期を再試行中', msg: '接続が不安定です。再接続しています。' },
error: { icon: '×', title: '削除に失敗', msg: '権限が不足しています。管理者に確認してください。' },
};
function showToast(type) {
if (!stack) return;
const p = PRESET[type] || PRESET.info;
const el = document.createElement('div');
el.className = `toast ${type}`;
el.style.setProperty('--life', LIFE + 'ms');
el.innerHTML =
`<span class="toast__icon">${p.icon}</span>` +
`<p class="toast__title">${p.title}</p>` +
`<p class="toast__msg">${p.msg}</p>` +
`<span class="toast__bar"></span>`;
stack.appendChild(el);
// 残り時間経過でスライドアウト→除去
const timer = setTimeout(() => dismiss(el), LIFE);
el.addEventListener('click', () => { clearTimeout(timer); dismiss(el); });
}
function dismiss(el) {
el.classList.add('is-out');
el.addEventListener('animationend', () => el.remove(), { once: true });
}
document.querySelectorAll('[data-toast]').forEach((btn) => {
btn.addEventListener('click', () => showToast(btn.dataset.toast));
});
コード
HTML
<!-- トースト:ボタンで種類別の通知をスタック表示、自動で消える -->
<div class="panel">
<h2 class="panel__title">トースト通知</h2>
<p class="panel__hint">ボタンを押すと右下に通知が積み重なります。</p>
<div class="panel__btns">
<button class="tbtn tbtn--ok" data-type="success">成功</button>
<button class="tbtn tbtn--info" data-type="info">お知らせ</button>
<button class="tbtn tbtn--warn" data-type="warn">警告</button>
<button class="tbtn tbtn--err" data-type="error">エラー</button>
</div>
</div>
<!-- 通知はここに動的に追加 -->
<div class="toasts" id="toasts" aria-live="polite" aria-atomic="false"></div>
CSS
:root{
--bg:#0d1117;
--card:#161b22;
--text:#e6edf3;
--muted:#8b949e;
--ok:#22c55e;
--info:#3b82f6;
--warn:#f59e0b;
--err:#ef4444;
}
*{box-sizing:border-box}
body{
margin:0;min-height:100vh;position:relative;overflow:hidden;
display:grid;place-items:center;padding:24px;
font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
background:radial-gradient(800px 400px at 50% -10%,#1b2436,transparent),var(--bg);
}
.panel{
width:min(420px,100%);text-align:center;
background:var(--card);border:1px solid #21262d;border-radius:18px;
padding:26px 22px;
}
.panel__title{margin:0 0 6px;font-size:1.2rem}
.panel__hint{margin:0 0 20px;color:var(--muted);font-size:.88rem}
.panel__btns{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
.tbtn{
font:inherit;font-weight:600;cursor:pointer;color:#fff;
border:none;border-radius:11px;padding:12px;
transition:transform .15s,filter .2s;
}
.tbtn:hover{filter:brightness(1.1)}
.tbtn:active{transform:translateY(1px)}
.tbtn--ok{background:var(--ok)}
.tbtn--info{background:var(--info)}
.tbtn--warn{background:var(--warn);color:#3a2a00}
.tbtn--err{background:var(--err)}
/* スタック容器(右下) */
.toasts{
position:fixed;right:18px;bottom:18px;z-index:50;
display:flex;flex-direction:column;gap:10px;width:min(300px,80vw);
}
.toast{
display:flex;align-items:flex-start;gap:10px;
background:var(--card);border:1px solid #2a313b;border-left:4px solid var(--c);
border-radius:12px;padding:12px 14px;color:var(--text);
box-shadow:0 16px 30px -16px rgba(0,0,0,.7);
animation:slideIn .35s cubic-bezier(.2,.9,.3,1.2);
position:relative;overflow:hidden;
}
.toast.is-leaving{animation:slideOut .3s ease forwards}
.toast__icon{
flex:none;width:22px;height:22px;border-radius:50%;
display:grid;place-items:center;font-size:.8rem;font-weight:700;
color:#fff;background:var(--c);
}
.toast__body{flex:1;min-width:0}
.toast__title{font-weight:700;font-size:.9rem}
.toast__msg{color:var(--muted);font-size:.8rem;margin-top:2px}
.toast__close{
background:none;border:none;color:var(--muted);cursor:pointer;
font-size:16px;line-height:1;padding:0 2px;
}
.toast__close:hover{color:var(--text)}
/* 残り時間バー */
.toast__bar{
position:absolute;left:0;bottom:0;height:3px;width:100%;
background:var(--c);transform-origin:left;
animation:shrink var(--dur,4s) linear forwards;
}
@keyframes slideIn{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
@keyframes slideOut{to{opacity:0;transform:translateX(40px)}}
@keyframes shrink{from{transform:scaleX(1)}to{transform:scaleX(0)}}
@media (prefers-reduced-motion:reduce){
.toast,.toast.is-leaving,.toast__bar{animation:none}
}
JavaScript
// トーストを動的生成して自動消滅させる
const host = document.getElementById('toasts');
// 種類ごとの見た目・文言
const PRESET = {
success: { c: 'var(--ok)', icon: '✓', title: '成功しました', msg: '変更内容を保存しました。' },
info: { c: 'var(--info)', icon: 'i', title: 'お知らせ', msg: '新しいバージョンが利用できます。' },
warn: { c: 'var(--warn)', icon: '!', title: 'ご注意ください', msg: '空き容量が少なくなっています。' },
error: { c: 'var(--err)', icon: '×', title: 'エラー', msg: '通信に失敗しました。再試行してください。' },
};
const DURATION = 4000; // 表示時間(ms)
const removeToast = (el) => {
if (!el || el.classList.contains('is-leaving')) return;
el.classList.add('is-leaving');
el.addEventListener('animationend', () => el.remove(), { once: true });
// 念のためのフォールバック
setTimeout(() => el.remove(), 400);
};
const showToast = (type) => {
if (!host) return;
const p = PRESET[type] || PRESET.info;
const el = document.createElement('div');
el.className = 'toast';
el.style.setProperty('--c', p.c);
el.style.setProperty('--dur', DURATION + 'ms');
el.innerHTML = `
<span class="toast__icon">${p.icon}</span>
<div class="toast__body">
<div class="toast__title">${p.title}</div>
<div class="toast__msg">${p.msg}</div>
</div>
<button class="toast__close" aria-label="閉じる">×</button>
<span class="toast__bar"></span>`;
el.querySelector('.toast__close').addEventListener('click', () => removeToast(el));
host.prepend(el);
// 古い通知が溜まりすぎないよう上限
while (host.children.length > 5) removeToast(host.lastElementChild);
setTimeout(() => removeToast(el), DURATION);
};
document.querySelectorAll('[data-type]').forEach((btn) => {
btn.addEventListener('click', () => showToast(btn.dataset.type));
});
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「トースト通知」の効果を追加してください。
# 追加してほしい効果
トースト通知(UIコンポーネント)
成功・警告など種類別の通知を右下にスタック表示し、残り時間バーとともに自動消滅。操作後のフィードバック表示に使えます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- トースト:ボタンで種類別の通知をスタック表示、自動で消える -->
<div class="panel">
<h2 class="panel__title">トースト通知</h2>
<p class="panel__hint">ボタンを押すと右下に通知が積み重なります。</p>
<div class="panel__btns">
<button class="tbtn tbtn--ok" data-type="success">成功</button>
<button class="tbtn tbtn--info" data-type="info">お知らせ</button>
<button class="tbtn tbtn--warn" data-type="warn">警告</button>
<button class="tbtn tbtn--err" data-type="error">エラー</button>
</div>
</div>
<!-- 通知はここに動的に追加 -->
<div class="toasts" id="toasts" aria-live="polite" aria-atomic="false"></div>
【CSS】
:root{
--bg:#0d1117;
--card:#161b22;
--text:#e6edf3;
--muted:#8b949e;
--ok:#22c55e;
--info:#3b82f6;
--warn:#f59e0b;
--err:#ef4444;
}
*{box-sizing:border-box}
body{
margin:0;min-height:100vh;position:relative;overflow:hidden;
display:grid;place-items:center;padding:24px;
font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
background:radial-gradient(800px 400px at 50% -10%,#1b2436,transparent),var(--bg);
}
.panel{
width:min(420px,100%);text-align:center;
background:var(--card);border:1px solid #21262d;border-radius:18px;
padding:26px 22px;
}
.panel__title{margin:0 0 6px;font-size:1.2rem}
.panel__hint{margin:0 0 20px;color:var(--muted);font-size:.88rem}
.panel__btns{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
.tbtn{
font:inherit;font-weight:600;cursor:pointer;color:#fff;
border:none;border-radius:11px;padding:12px;
transition:transform .15s,filter .2s;
}
.tbtn:hover{filter:brightness(1.1)}
.tbtn:active{transform:translateY(1px)}
.tbtn--ok{background:var(--ok)}
.tbtn--info{background:var(--info)}
.tbtn--warn{background:var(--warn);color:#3a2a00}
.tbtn--err{background:var(--err)}
/* スタック容器(右下) */
.toasts{
position:fixed;right:18px;bottom:18px;z-index:50;
display:flex;flex-direction:column;gap:10px;width:min(300px,80vw);
}
.toast{
display:flex;align-items:flex-start;gap:10px;
background:var(--card);border:1px solid #2a313b;border-left:4px solid var(--c);
border-radius:12px;padding:12px 14px;color:var(--text);
box-shadow:0 16px 30px -16px rgba(0,0,0,.7);
animation:slideIn .35s cubic-bezier(.2,.9,.3,1.2);
position:relative;overflow:hidden;
}
.toast.is-leaving{animation:slideOut .3s ease forwards}
.toast__icon{
flex:none;width:22px;height:22px;border-radius:50%;
display:grid;place-items:center;font-size:.8rem;font-weight:700;
color:#fff;background:var(--c);
}
.toast__body{flex:1;min-width:0}
.toast__title{font-weight:700;font-size:.9rem}
.toast__msg{color:var(--muted);font-size:.8rem;margin-top:2px}
.toast__close{
background:none;border:none;color:var(--muted);cursor:pointer;
font-size:16px;line-height:1;padding:0 2px;
}
.toast__close:hover{color:var(--text)}
/* 残り時間バー */
.toast__bar{
position:absolute;left:0;bottom:0;height:3px;width:100%;
background:var(--c);transform-origin:left;
animation:shrink var(--dur,4s) linear forwards;
}
@keyframes slideIn{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
@keyframes slideOut{to{opacity:0;transform:translateX(40px)}}
@keyframes shrink{from{transform:scaleX(1)}to{transform:scaleX(0)}}
@media (prefers-reduced-motion:reduce){
.toast,.toast.is-leaving,.toast__bar{animation:none}
}
【JavaScript】
// トーストを動的生成して自動消滅させる
const host = document.getElementById('toasts');
// 種類ごとの見た目・文言
const PRESET = {
success: { c: 'var(--ok)', icon: '✓', title: '成功しました', msg: '変更内容を保存しました。' },
info: { c: 'var(--info)', icon: 'i', title: 'お知らせ', msg: '新しいバージョンが利用できます。' },
warn: { c: 'var(--warn)', icon: '!', title: 'ご注意ください', msg: '空き容量が少なくなっています。' },
error: { c: 'var(--err)', icon: '×', title: 'エラー', msg: '通信に失敗しました。再試行してください。' },
};
const DURATION = 4000; // 表示時間(ms)
const removeToast = (el) => {
if (!el || el.classList.contains('is-leaving')) return;
el.classList.add('is-leaving');
el.addEventListener('animationend', () => el.remove(), { once: true });
// 念のためのフォールバック
setTimeout(() => el.remove(), 400);
};
const showToast = (type) => {
if (!host) return;
const p = PRESET[type] || PRESET.info;
const el = document.createElement('div');
el.className = 'toast';
el.style.setProperty('--c', p.c);
el.style.setProperty('--dur', DURATION + 'ms');
el.innerHTML = `
<span class="toast__icon">${p.icon}</span>
<div class="toast__body">
<div class="toast__title">${p.title}</div>
<div class="toast__msg">${p.msg}</div>
</div>
<button class="toast__close" aria-label="閉じる">×</button>
<span class="toast__bar"></span>`;
el.querySelector('.toast__close').addEventListener('click', () => removeToast(el));
host.prepend(el);
// 古い通知が溜まりすぎないよう上限
while (host.children.length > 5) removeToast(host.lastElementChild);
setTimeout(() => removeToast(el), DURATION);
};
document.querySelectorAll('[data-type]').forEach((btn) => {
btn.addEventListener('click', () => showToast(btn.dataset.type));
});
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。