エラー状態
雲とアイコンで接続エラーを穏やかに表現する空状態。再読み込みを押すと読み込み中を経て一時的に復旧表示へ切り替わり、繰り返し試せます。
ライブデモ
コード
HTML
<!-- 空状態: エラー(接続不可)。雲+アイコンが静かに呼吸し、再読み込みで一時的に回復表示へ切り替わる -->
<main class="eer-card" data-state="error">
<svg class="eer-illus" viewBox="0 0 200 170" width="168" height="143" role="img" aria-label="データを読み込めなかったことを示す、雲と警告マークのイラスト">
<path class="eer-cloud" d="M56 100 C40 100 28 88 28 73 C28 58 40 46 55 46 C58 30 72 18 90 18 C108 18 123 31 125 47 C140 48 152 60 152 75 C152 89 141 100 127 100 Z"></path>
<g class="eer-mark eer-mark--error">
<circle class="eer-mark-glow" cx="90" cy="60" r="23"></circle>
<path class="eer-bang" d="M90 48 V66 M90 74 V75"></path>
</g>
<g class="eer-mark eer-mark--ok">
<circle class="eer-mark-glow" cx="90" cy="60" r="23"></circle>
<path class="eer-check" d="M79 61 L88 70 L103 51"></path>
</g>
<g class="eer-signal" aria-hidden="true">
<rect x="136" y="138" width="7" height="12" rx="2"></rect>
<rect x="148" y="128" width="7" height="22" rx="2"></rect>
<rect class="eer-signal-weak" x="160" y="118" width="7" height="32" rx="2"></rect>
</g>
</svg>
<h2 class="eer-title" data-error="問題が発生しました" data-ok="復旧しました">問題が発生しました</h2>
<p class="eer-desc"
data-error="一時的にデータを読み込めませんでした。ネットワーク接続を確認して、もう一度お試しください。"
data-ok="接続が回復しました。最新のデータを表示しています。">一時的にデータを読み込めませんでした。ネットワーク接続を確認して、もう一度お試しください。</p>
<button class="eer-cta" type="button" data-cta>
<span class="eer-spinner" aria-hidden="true"></span>
<span class="eer-cta-label" data-label data-default="再読み込み" data-busy="再読み込み中…">再読み込み</span>
</button>
</main>
CSS
/* 空状態: エラー(接続不可) */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 32px 20px;
font-family: "Hiragino Sans", "Yu Gothic UI", "Segoe UI", system-ui, -apple-system, sans-serif;
background:
radial-gradient(circle at 18% 16%, rgba(251, 113, 133, .14) 0%, transparent 46%),
radial-gradient(circle at 86% 84%, rgba(253, 186, 116, .18) 0%, transparent 50%),
#fef6f4;
color: #9f1239;
}
.eer-card {
width: 100%;
max-width: 380px;
background: #ffffff;
border-radius: 24px;
padding: 40px 32px 34px;
text-align: center;
box-shadow: 0 24px 60px -24px rgba(225, 29, 72, .22), 0 2px 8px rgba(159, 18, 57, .06);
border: 1px solid rgba(251, 113, 133, .14);
}
.eer-illus { display: block; margin: 0 auto 8px; overflow: visible; }
.eer-cloud {
fill: rgba(244, 63, 94, .05);
stroke: #fda4af;
stroke-width: 4;
stroke-linejoin: round;
animation: eer-drift 4.2s ease-in-out infinite;
transform-box: fill-box;
transform-origin: center;
}
@keyframes eer-drift {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
.eer-mark { transform-box: fill-box; transform-origin: center; transition: opacity .25s ease; }
.eer-mark--error { opacity: 1; animation: eer-pulse 2.4s ease-in-out infinite; }
.eer-mark--ok { opacity: 0; }
@keyframes eer-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.07); }
}
.eer-mark--error .eer-mark-glow { fill: #fee2e2; stroke: #fb7185; stroke-width: 2.5; }
.eer-mark--error .eer-bang { fill: none; stroke: #be123c; stroke-width: 7; stroke-linecap: round; }
.eer-mark--ok .eer-mark-glow { fill: #d1fae5; stroke: #34d399; stroke-width: 2.5; }
.eer-mark--ok .eer-check { fill: none; stroke: #047857; stroke-width: 7; stroke-linecap: round; stroke-linejoin: round; }
.eer-signal rect { fill: #fda4af; transition: fill .3s ease, opacity .3s ease; }
.eer-signal-weak { fill: #e5e7eb; opacity: .8; }
.eer-title { margin: 6px 0 10px; font-size: 21px; font-weight: 700; color: #9f1239; transition: opacity .2s ease; }
.eer-desc { margin: 0 0 24px; font-size: 14.5px; line-height: 1.7; color: #a9677c; transition: opacity .2s ease; }
.eer-cta {
appearance: none;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 13px 30px;
font-size: 15px;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #fb7185 0%, #e11d48 100%);
border-radius: 999px;
box-shadow: 0 12px 26px -10px rgba(225, 29, 72, .5);
transition: transform .25s cubic-bezier(.22,1,.36,1), box-shadow .25s ease, opacity .2s ease;
}
.eer-cta:hover { transform: translateY(-2px); box-shadow: 0 16px 32px -10px rgba(225, 29, 72, .6); }
.eer-cta:active { transform: translateY(0); }
.eer-cta:disabled { opacity: .8; cursor: default; transform: none; }
.eer-spinner {
display: none;
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, .4);
border-top-color: #fff;
}
.eer-cta.is-loading .eer-spinner { display: inline-block; animation: eer-spin .7s linear infinite; }
@keyframes eer-spin { to { transform: rotate(360deg); } }
/* 復旧(成功)状態: 色味とマークを差し替える */
.eer-card[data-state="ok"] .eer-cloud { stroke: #6ee7b7; }
.eer-card[data-state="ok"] .eer-mark--error { opacity: 0; }
.eer-card[data-state="ok"] .eer-mark--ok { opacity: 1; }
.eer-card[data-state="ok"] .eer-signal rect { fill: #34d399; opacity: 1; }
.eer-card.is-swapping .eer-title,
.eer-card.is-swapping .eer-desc { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
.eer-cloud, .eer-mark--error, .eer-spinner { animation: none !important; }
}
JavaScript
// 空状態: エラー(接続不可) — 「再読み込み」で読込中→一時的に復旧表示→数秒後に元のエラー表示へ戻る(繰り返し試せるデモ)
(() => {
const card = document.querySelector('.eer-card');
if (!card) return; // null安全
const cta = card.querySelector('[data-cta]');
const title = card.querySelector('.eer-title');
const desc = card.querySelector('.eer-desc');
const label = card.querySelector('[data-label]');
if (!cta || !title || !desc || !label) return;
let busy = false;
let revertTimer = null;
let swapTimer = null;
const setTexts = (state) => {
title.textContent = title.dataset[state] || title.textContent;
desc.textContent = desc.dataset[state] || desc.textContent;
};
cta.addEventListener('click', () => {
if (busy) return;
busy = true;
if (revertTimer) clearTimeout(revertTimer);
// 1) 読み込み中
cta.classList.add('is-loading');
cta.disabled = true;
label.textContent = label.dataset.busy || label.textContent;
setTimeout(() => {
// 2) 一時的に復旧状態を表示
card.dataset.state = 'ok';
card.classList.add('is-swapping');
swapTimer = setTimeout(() => {
setTexts('ok');
card.classList.remove('is-swapping');
}, 140);
cta.classList.remove('is-loading');
cta.disabled = false;
label.textContent = label.dataset.default || label.textContent;
// 3) しばらくしたら元のエラー表示へ戻す(デモを何度も試せるように)
revertTimer = setTimeout(() => {
card.dataset.state = 'error';
card.classList.add('is-swapping');
if (swapTimer) clearTimeout(swapTimer);
swapTimer = setTimeout(() => {
setTexts('error');
card.classList.remove('is-swapping');
}, 140);
busy = false;
}, 2600);
}, 1000);
});
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「エラー状態」の効果を追加してください。
# 追加してほしい効果
エラー状態(アラート & 空状態)
雲とアイコンで接続エラーを穏やかに表現する空状態。再読み込みを押すと読み込み中を経て一時的に復旧表示へ切り替わり、繰り返し試せます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 空状態: エラー(接続不可)。雲+アイコンが静かに呼吸し、再読み込みで一時的に回復表示へ切り替わる -->
<main class="eer-card" data-state="error">
<svg class="eer-illus" viewBox="0 0 200 170" width="168" height="143" role="img" aria-label="データを読み込めなかったことを示す、雲と警告マークのイラスト">
<path class="eer-cloud" d="M56 100 C40 100 28 88 28 73 C28 58 40 46 55 46 C58 30 72 18 90 18 C108 18 123 31 125 47 C140 48 152 60 152 75 C152 89 141 100 127 100 Z"></path>
<g class="eer-mark eer-mark--error">
<circle class="eer-mark-glow" cx="90" cy="60" r="23"></circle>
<path class="eer-bang" d="M90 48 V66 M90 74 V75"></path>
</g>
<g class="eer-mark eer-mark--ok">
<circle class="eer-mark-glow" cx="90" cy="60" r="23"></circle>
<path class="eer-check" d="M79 61 L88 70 L103 51"></path>
</g>
<g class="eer-signal" aria-hidden="true">
<rect x="136" y="138" width="7" height="12" rx="2"></rect>
<rect x="148" y="128" width="7" height="22" rx="2"></rect>
<rect class="eer-signal-weak" x="160" y="118" width="7" height="32" rx="2"></rect>
</g>
</svg>
<h2 class="eer-title" data-error="問題が発生しました" data-ok="復旧しました">問題が発生しました</h2>
<p class="eer-desc"
data-error="一時的にデータを読み込めませんでした。ネットワーク接続を確認して、もう一度お試しください。"
data-ok="接続が回復しました。最新のデータを表示しています。">一時的にデータを読み込めませんでした。ネットワーク接続を確認して、もう一度お試しください。</p>
<button class="eer-cta" type="button" data-cta>
<span class="eer-spinner" aria-hidden="true"></span>
<span class="eer-cta-label" data-label data-default="再読み込み" data-busy="再読み込み中…">再読み込み</span>
</button>
</main>
【CSS】
/* 空状態: エラー(接続不可) */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 32px 20px;
font-family: "Hiragino Sans", "Yu Gothic UI", "Segoe UI", system-ui, -apple-system, sans-serif;
background:
radial-gradient(circle at 18% 16%, rgba(251, 113, 133, .14) 0%, transparent 46%),
radial-gradient(circle at 86% 84%, rgba(253, 186, 116, .18) 0%, transparent 50%),
#fef6f4;
color: #9f1239;
}
.eer-card {
width: 100%;
max-width: 380px;
background: #ffffff;
border-radius: 24px;
padding: 40px 32px 34px;
text-align: center;
box-shadow: 0 24px 60px -24px rgba(225, 29, 72, .22), 0 2px 8px rgba(159, 18, 57, .06);
border: 1px solid rgba(251, 113, 133, .14);
}
.eer-illus { display: block; margin: 0 auto 8px; overflow: visible; }
.eer-cloud {
fill: rgba(244, 63, 94, .05);
stroke: #fda4af;
stroke-width: 4;
stroke-linejoin: round;
animation: eer-drift 4.2s ease-in-out infinite;
transform-box: fill-box;
transform-origin: center;
}
@keyframes eer-drift {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
.eer-mark { transform-box: fill-box; transform-origin: center; transition: opacity .25s ease; }
.eer-mark--error { opacity: 1; animation: eer-pulse 2.4s ease-in-out infinite; }
.eer-mark--ok { opacity: 0; }
@keyframes eer-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.07); }
}
.eer-mark--error .eer-mark-glow { fill: #fee2e2; stroke: #fb7185; stroke-width: 2.5; }
.eer-mark--error .eer-bang { fill: none; stroke: #be123c; stroke-width: 7; stroke-linecap: round; }
.eer-mark--ok .eer-mark-glow { fill: #d1fae5; stroke: #34d399; stroke-width: 2.5; }
.eer-mark--ok .eer-check { fill: none; stroke: #047857; stroke-width: 7; stroke-linecap: round; stroke-linejoin: round; }
.eer-signal rect { fill: #fda4af; transition: fill .3s ease, opacity .3s ease; }
.eer-signal-weak { fill: #e5e7eb; opacity: .8; }
.eer-title { margin: 6px 0 10px; font-size: 21px; font-weight: 700; color: #9f1239; transition: opacity .2s ease; }
.eer-desc { margin: 0 0 24px; font-size: 14.5px; line-height: 1.7; color: #a9677c; transition: opacity .2s ease; }
.eer-cta {
appearance: none;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 13px 30px;
font-size: 15px;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #fb7185 0%, #e11d48 100%);
border-radius: 999px;
box-shadow: 0 12px 26px -10px rgba(225, 29, 72, .5);
transition: transform .25s cubic-bezier(.22,1,.36,1), box-shadow .25s ease, opacity .2s ease;
}
.eer-cta:hover { transform: translateY(-2px); box-shadow: 0 16px 32px -10px rgba(225, 29, 72, .6); }
.eer-cta:active { transform: translateY(0); }
.eer-cta:disabled { opacity: .8; cursor: default; transform: none; }
.eer-spinner {
display: none;
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, .4);
border-top-color: #fff;
}
.eer-cta.is-loading .eer-spinner { display: inline-block; animation: eer-spin .7s linear infinite; }
@keyframes eer-spin { to { transform: rotate(360deg); } }
/* 復旧(成功)状態: 色味とマークを差し替える */
.eer-card[data-state="ok"] .eer-cloud { stroke: #6ee7b7; }
.eer-card[data-state="ok"] .eer-mark--error { opacity: 0; }
.eer-card[data-state="ok"] .eer-mark--ok { opacity: 1; }
.eer-card[data-state="ok"] .eer-signal rect { fill: #34d399; opacity: 1; }
.eer-card.is-swapping .eer-title,
.eer-card.is-swapping .eer-desc { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
.eer-cloud, .eer-mark--error, .eer-spinner { animation: none !important; }
}
【JavaScript】
// 空状態: エラー(接続不可) — 「再読み込み」で読込中→一時的に復旧表示→数秒後に元のエラー表示へ戻る(繰り返し試せるデモ)
(() => {
const card = document.querySelector('.eer-card');
if (!card) return; // null安全
const cta = card.querySelector('[data-cta]');
const title = card.querySelector('.eer-title');
const desc = card.querySelector('.eer-desc');
const label = card.querySelector('[data-label]');
if (!cta || !title || !desc || !label) return;
let busy = false;
let revertTimer = null;
let swapTimer = null;
const setTexts = (state) => {
title.textContent = title.dataset[state] || title.textContent;
desc.textContent = desc.dataset[state] || desc.textContent;
};
cta.addEventListener('click', () => {
if (busy) return;
busy = true;
if (revertTimer) clearTimeout(revertTimer);
// 1) 読み込み中
cta.classList.add('is-loading');
cta.disabled = true;
label.textContent = label.dataset.busy || label.textContent;
setTimeout(() => {
// 2) 一時的に復旧状態を表示
card.dataset.state = 'ok';
card.classList.add('is-swapping');
swapTimer = setTimeout(() => {
setTexts('ok');
card.classList.remove('is-swapping');
}, 140);
cta.classList.remove('is-loading');
cta.disabled = false;
label.textContent = label.dataset.default || label.textContent;
// 3) しばらくしたら元のエラー表示へ戻す(デモを何度も試せるように)
revertTimer = setTimeout(() => {
card.dataset.state = 'error';
card.classList.add('is-swapping');
if (swapTimer) clearTimeout(swapTimer);
swapTimer = setTimeout(() => {
setTexts('error');
card.classList.remove('is-swapping');
}, 140);
busy = false;
}, 2600);
}, 1000);
});
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。