APIエラーバナー(API Error Banner)
このコンポーネントについて
APIエラーバナーは、fetch 等のAPI通信が失敗したときにユーザーへ原因と次の行動を伝えるUIコンポーネントです。トースト通知と異なり、バナーはボタンの直下に固定表示され、ユーザーが「再試行」するまで消えません。
エラーの種類(タイムアウト・認証エラー・サーバーエラー・接続断)によって表示内容を出し分けることで、適切なフィードバックを提供できます。業務アプリのデータ取得・送信処理に組み込まれる頻出パターンです。
- ローディング表示 — ボタン押下後にスピナーを表示し、通信中であることを伝える
- エラーバナー表示 — スピナーが消えた後、ボタン直下にエラー内容のバナーを表示する
- 4種のエラーパターン循環 — ボタンを押すたびに「タイムアウト → 403 → 500 → 接続断」の順でパターンが切り替わる
- 再試行ボタン — バナー内の再試行ボタンを押すと同じサイクルを繰り返す(ローディング → 次のエラー)
- エラー種別の色分け — 重大度に応じてバナーの色を変える(警告:オレンジ / エラー:赤 / 接続断:グレー)
実装のポイント・注意点
バナーの表示切り替えには hidden 属性を使います。CSS の display: none を書かなくても element.removeAttribute('hidden') / element.setAttribute('hidden', '') で ON/OFF できます。
エラーバナーの色を種別ごとに切り替える際は、前回のクラスを必ずリセットしてから新しいクラスを付与します。classList.remove('aeb-banner--warning', 'aeb-banner--error', 'aeb-banner--network') のようにすべての種別クラスを一括削除するか、className を直接上書きするのが安全です。前回のクラスが残ると複数の背景色が混在してしまいます。
スピナーアニメーションのキーフレーム名には aeb-spin のようにコンポーネント固有のプレフィックスを付けます。グローバルな spin や rotate という名前を使うと、他のコンポーネントのアニメーションと衝突する恐れがあります。
ボタンの連打対策として、triggerRequest() の先頭でボタンが disabled かどうかをチェックします。disabled の間は処理を抜けるだけで済み、フラグ変数より読みやすいコードになります。
HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。
デモ
ボタンを押すたびにエラー内容が変わります
サンプルソース
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>APIエラーバナー サンプル</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="aeb-wrapper">
<!-- 案内テキスト -->
<p class="aeb-guide">ボタンを押すたびにエラー内容が変わります</p>
<!-- トリガーボタン -->
<button class="aeb-trigger" id="aebTrigger">APIを叩く</button>
<!-- スピナー -->
<div class="aeb-spinner" id="aebSpinner" hidden>
<span class="aeb-spinner-icon" aria-hidden="true"></span>
<span class="aeb-spinner-text">通信中...</span>
</div>
<!-- エラーバナー -->
<div class="aeb-banner" id="aebBanner" role="alert" hidden>
<svg class="aeb-banner-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div class="aeb-banner-body">
<p class="aeb-banner-title" id="aebTitle"></p>
<p class="aeb-banner-desc" id="aebDesc"></p>
</div>
<button class="aeb-retry-btn" id="aebRetry">再試行</button>
</div>
</div>
<script src="./script.js"></script>
</body>
</html>
/* ===== CSS変数(色の調整はここで)===== */
:root {
--aeb-primary: #2B7FE8;
--aeb-primary-hover: #1A6FD8;
--aeb-text: #1A2332;
--aeb-text-muted: #5A6A7A;
--aeb-border: #D0D7E0;
--aeb-spinner-color: #9AA5B4;
/* 警告(タイムアウト)*/
--aeb-warn-bg: #FFF7ED;
--aeb-warn-border: #F97316;
--aeb-warn-text: #7C2D12;
/* エラー(403/500)*/
--aeb-err-bg: #FEF2F2;
--aeb-err-border: #EF4444;
--aeb-err-text: #7F1D1D;
/* 接続断 */
--aeb-net-bg: #F4F6F9;
--aeb-net-border: #9AA5B4;
--aeb-net-text: #374151;
}
/* hidden 属性を display:flex が上書きしないよう明示的に打ち消す */
.aeb-spinner[hidden],
.aeb-banner[hidden] {
display: none;
}
*, *::before, *::after { box-sizing: border-box; }
body { font-family: sans-serif; padding: 32px 24px; background: #F4F6F9; }
/* ===== ラッパー ===== */
.aeb-wrapper {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 480px;
margin: 0 auto;
}
/* ===== 案内テキスト ===== */
.aeb-guide {
margin: 0;
font-size: 13px;
color: var(--aeb-text-muted);
}
/* ===== トリガーボタン ===== */
.aeb-trigger {
align-self: flex-start;
padding: 9px 20px;
font-size: 14px;
font-weight: 600;
color: #fff;
background: var(--aeb-primary);
border: none;
border-radius: 6px;
cursor: pointer;
font-family: sans-serif;
transition: background 0.15s, opacity 0.15s;
}
.aeb-trigger:hover:not(:disabled) {
background: var(--aeb-primary-hover);
}
.aeb-trigger:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ===== スピナー ===== */
.aeb-spinner {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--aeb-text-muted);
}
.aeb-spinner-icon {
display: inline-block;
width: 18px;
height: 18px;
border: 2.5px solid var(--aeb-border);
border-top-color: var(--aeb-spinner-color);
border-radius: 50%;
animation: aeb-spin 0.7s linear infinite;
flex-shrink: 0;
}
@keyframes aeb-spin {
to { transform: rotate(360deg); }
}
/* ===== エラーバナー(共通)===== */
.aeb-banner {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 16px;
border-radius: 8px;
border-left: 4px solid transparent;
}
.aeb-banner-icon {
flex-shrink: 0;
margin-top: 1px;
}
.aeb-banner-body {
flex: 1;
min-width: 0;
}
.aeb-banner-title {
margin: 0 0 3px;
font-size: 14px;
font-weight: 700;
}
.aeb-banner-desc {
margin: 0;
font-size: 13px;
line-height: 1.5;
}
/* ===== エラー種別カラー ===== */
.aeb-banner--warning {
background: var(--aeb-warn-bg);
border-color: var(--aeb-warn-border);
color: var(--aeb-warn-text);
}
.aeb-banner--error {
background: var(--aeb-err-bg);
border-color: var(--aeb-err-border);
color: var(--aeb-err-text);
}
.aeb-banner--network {
background: var(--aeb-net-bg);
border-color: var(--aeb-net-border);
color: var(--aeb-net-text);
}
/* ===== 再試行ボタン ===== */
.aeb-retry-btn {
flex-shrink: 0;
padding: 5px 12px;
font-size: 12px;
font-weight: 600;
background: #fff;
border: 1.5px solid currentColor;
border-radius: 5px;
cursor: pointer;
font-family: sans-serif;
color: inherit;
opacity: 0.85;
transition: opacity 0.15s;
white-space: nowrap;
}
.aeb-retry-btn:hover {
opacity: 1;
}
// エラーパターン定義(順番に循環する)
var ERROR_PATTERNS = [
{
title: 'タイムアウトエラー',
desc: 'サーバーが応答しません。時間をおいて再試行してください。',
type: 'warning'
},
{
title: '403 Forbidden',
desc: 'アクセス権限がありません。ログイン状態を確認してください。',
type: 'error'
},
{
title: '500 Server Error',
desc: 'サーバーで問題が発生しました。しばらく経ってから再試行してください。',
type: 'error'
},
{
title: '接続断(Network Error)',
desc: 'ネットワーク接続を確認してください。',
type: 'network'
}
];
var currentIndex = 0;
var aebTrigger = document.getElementById('aebTrigger');
var aebSpinner = document.getElementById('aebSpinner');
var aebBanner = document.getElementById('aebBanner');
var aebTitle = document.getElementById('aebTitle');
var aebDesc = document.getElementById('aebDesc');
var aebRetry = document.getElementById('aebRetry');
// ボタン押下で通信を模擬してエラーを表示
function triggerRequest() {
// 連打防止:disabled の間は処理しない
if (aebTrigger.disabled) return;
// ローディング開始
aebTrigger.disabled = true;
aebBanner.setAttribute('hidden', '');
aebSpinner.removeAttribute('hidden');
// 1.5秒後にエラーバナーを表示
setTimeout(function() {
aebSpinner.setAttribute('hidden', '');
showError(ERROR_PATTERNS[currentIndex]);
// 次回は次のパターンへ循環
currentIndex = (currentIndex + 1) % ERROR_PATTERNS.length;
aebTrigger.disabled = false;
}, 1500);
}
// エラーバナーを表示する
function showError(pattern) {
// 前回の種別クラスをリセットしてから付与
aebBanner.classList.remove('aeb-banner--warning', 'aeb-banner--error', 'aeb-banner--network');
aebBanner.classList.add('aeb-banner--' + pattern.type);
// textContent で安全に代入(innerHTML に変数を結合しない)
aebTitle.textContent = pattern.title;
aebDesc.textContent = pattern.desc;
aebBanner.removeAttribute('hidden');
}
aebTrigger.addEventListener('click', triggerRequest);
aebRetry.addEventListener('click', triggerRequest);
AI用プロンプト
以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。
ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリ指定や列数変更など、要件を追記して使うのがおすすめです。
※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。
💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。
# APIエラーバナー 作成依頼
## 概要
fetch等のAPI通信が失敗したときに、ボタンの直下にエラー内容を示すバナーを表示するコンポーネントを作成してください。
## 要件
- ボタンを押すと約1.5秒間スピナー(ローディング表示)を出し、その後エラーバナーを表示する
- エラーバナーはボタンの直下に表示する(fixed/stickyではない)
- ボタンを押すたびに「タイムアウト → 403 Forbidden → 500 Server Error → 接続断」の順でエラーパターンが循環する
- エラーバナー内に「再試行」ボタンを置き、押すと同じサイクルを繰り返す
- エラーの重大度に応じてバナーの色を変える(警告:オレンジ / エラー:赤 / 接続断:グレー)
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:不要
## 動作詳細
- スピナーはCSSアニメーション(border回転)で実装する
- 表示切り替えは hidden 属性を使う
- エラーパターンは配列で管理し、インデックスを循環させる
- バナーの色はエラー種別クラス(warning / error / network)で切り替える
- エラータイトル・説明文の代入は textContent を使うこと(XSS対策)
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。