Empty States — Something-went-wrong state
A polished, self-contained error state for failed data loads, built with vanilla HTML, CSS, and JavaScript. It shows a warning illustration, a clear headline, an explanatory line and a copyable faux error code, plus a primary Try-again action that spins and resolves into a success state with a mini recovery chart. A segmented control swaps between server, network, and not-found framings as well as inline-card and full-page layouts.
MCP
Код
:root {
--brand: #5b5bf0;
--brand-d: #4646d6;
--brand-700: #3a3ab8;
--brand-50: #eef0ff;
--accent: #00b4a6;
--accent-soft: #d8f5f2;
--ink: #101322;
--ink-2: #3a4060;
--muted: #6c7393;
--bg: #f6f7fb;
--white: #ffffff;
--surface: #ffffff;
--line: rgba(16, 19, 34, 0.1);
--line-2: rgba(16, 19, 34, 0.16);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-sm: 0 1px 2px rgba(16, 19, 34, 0.08);
--sh-lg: 0 8px 24px rgba(16, 19, 34, 0.08);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.page {
max-width: 860px;
margin: 0 auto;
padding: 40px 20px 72px;
}
.page__head { margin-bottom: 24px; }
.eyebrow {
display: inline-block;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--brand);
background: var(--brand-50);
padding: 4px 10px;
border-radius: 999px;
}
.page__head h1 {
margin: 14px 0 6px;
font-size: 28px;
font-weight: 800;
letter-spacing: -0.02em;
}
.lede {
margin: 0;
max-width: 56ch;
color: var(--muted);
font-size: 15px;
}
/* ---- Controls ---- */
.controls {
display: flex;
flex-wrap: wrap;
gap: 14px;
margin-bottom: 22px;
}
.seg {
display: flex;
align-items: center;
gap: 10px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 8px 10px;
box-shadow: var(--sh-sm);
}
.seg__label {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
.seg__btns {
display: inline-flex;
background: var(--bg);
border-radius: var(--r-sm);
padding: 3px;
}
.seg__btn {
appearance: none;
border: none;
background: transparent;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
}
.seg__btn:hover { color: var(--ink); }
.seg__btn.is-active {
background: var(--white);
color: var(--brand-700);
box-shadow: var(--sh-sm);
}
.seg__btn:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
/* ---- Stage / panel ---- */
.stage { transition: padding 0.2s; }
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-lg);
overflow: hidden;
}
.panel__bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid var(--line);
background: linear-gradient(180deg, rgba(91, 91, 240, 0.03), transparent);
}
.panel__title {
display: flex;
align-items: center;
gap: 9px;
font-weight: 700;
font-size: 14px;
}
.dot { width: 9px; height: 9px; border-radius: 50%; }
.dot--a { background: var(--brand); box-shadow: 0 0 0 3px var(--brand-50); }
.panel__meta { font-size: 12px; color: var(--muted); font-weight: 500; }
.panel__body {
display: flex;
align-items: center;
justify-content: center;
min-height: 380px;
padding: 40px 24px;
}
/* ---- State block ---- */
.state {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
max-width: 420px;
animation: rise 0.32s cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
@keyframes rise {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.art { margin-bottom: 18px; }
.art__svg { width: 96px; height: 96px; display: block; }
.art__ring {
stroke: var(--line-2);
stroke-width: 2;
fill: rgba(212, 80, 62, 0.04);
}
.art__tri {
stroke: var(--danger);
stroke-width: 4.5;
stroke-linejoin: round;
fill: rgba(212, 80, 62, 0.1);
}
.art__bang, .art__dot { stroke: var(--danger); fill: var(--danger); stroke-width: 4.5; stroke-linecap: round; }
.art--ok .art__ring--ok { stroke: rgba(47, 158, 111, 0.4); fill: rgba(47, 158, 111, 0.06); }
.art__check {
stroke: var(--ok);
stroke-width: 5;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
stroke-dasharray: 80;
stroke-dashoffset: 80;
animation: draw 0.5s 0.05s ease forwards;
}
@keyframes draw { to { stroke-dashoffset: 0; } }
.state__title {
margin: 0 0 8px;
font-size: 20px;
font-weight: 800;
letter-spacing: -0.01em;
}
.state__desc {
margin: 0 0 16px;
color: var(--muted);
font-size: 14.5px;
}
/* ---- Error code chip ---- */
.errcode {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0 0 22px;
padding: 6px 8px 6px 12px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 999px;
font-size: 12.5px;
}
.errcode__k {
font-weight: 700;
color: var(--danger);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 11px;
}
.errcode code {
font-family: "SF Mono", ui-monospace, "Menlo", monospace;
color: var(--ink-2);
font-size: 12px;
}
.copy {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border: none;
background: var(--white);
color: var(--muted);
border-radius: 6px;
cursor: pointer;
box-shadow: var(--sh-sm);
transition: color 0.15s, transform 0.1s;
}
.copy:hover { color: var(--brand); }
.copy:active { transform: scale(0.92); }
.copy:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* ---- Actions / buttons ---- */
.state__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
font: inherit;
font-size: 14px;
font-weight: 600;
padding: 10px 18px;
border-radius: var(--r-sm);
cursor: pointer;
text-decoration: none;
border: 1px solid transparent;
transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.08s, box-shadow 0.15s;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.btn--primary {
background: var(--brand);
color: var(--white);
box-shadow: var(--sh-sm);
}
.btn--primary:hover { background: var(--brand-d); }
.btn--ghost {
background: var(--white);
color: var(--ink-2);
border-color: var(--line-2);
}
.btn--ghost:hover { background: var(--bg); color: var(--ink); }
.btn__ic { flex-shrink: 0; }
/* spinner inside Try again */
.btn__spin {
display: none;
width: 15px;
height: 15px;
border: 2px solid rgba(255, 255, 255, 0.4);
border-top-color: var(--white);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.btn.is-loading { pointer-events: none; opacity: 0.92; }
.btn.is-loading .btn__spin { display: inline-block; }
.btn.is-loading .btn__ic { display: none; }
.retry-note {
min-height: 18px;
margin: 14px 0 0;
font-size: 12.5px;
color: var(--muted);
}
.retry-note.is-error { color: var(--danger); font-weight: 600; }
/* ---- Success mini chart ---- */
.success-chart {
display: flex;
align-items: flex-end;
gap: 8px;
height: 70px;
margin: 6px 0 22px;
}
.success-chart span {
width: 18px;
height: var(--h);
background: linear-gradient(180deg, var(--accent), var(--ok));
border-radius: 4px 4px 2px 2px;
animation: grow 0.5s cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
.success-chart span:nth-child(2) { animation-delay: 0.05s; }
.success-chart span:nth-child(3) { animation-delay: 0.1s; }
.success-chart span:nth-child(4) { animation-delay: 0.15s; }
.success-chart span:nth-child(5) { animation-delay: 0.2s; }
.success-chart span:nth-child(6) { animation-delay: 0.25s; }
.success-chart span:nth-child(7) { animation-delay: 0.3s; }
@keyframes grow { from { height: 0; opacity: 0; } }
/* ---- Full-page variant ---- */
.stage[data-layout="full"] .panel { border-radius: 0; box-shadow: none; border: none; background: transparent; }
.stage[data-layout="full"] .panel__bar { display: none; }
.stage[data-layout="full"] .panel__body {
min-height: 460px;
background:
radial-gradient(120% 90% at 50% -10%, rgba(91, 91, 240, 0.07), transparent 60%),
var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-lg);
padding: 56px 24px;
}
.stage[data-layout="full"] .art__svg { width: 120px; height: 120px; }
.stage[data-layout="full"] .state { max-width: 460px; }
.stage[data-layout="full"] .state__title { font-size: 24px; }
/* ---- Toast ---- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%) translateY(24px);
background: var(--ink);
color: var(--white);
font-size: 13.5px;
font-weight: 600;
padding: 11px 18px;
border-radius: 999px;
box-shadow: 0 10px 30px rgba(16, 19, 34, 0.28);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s, transform 0.22s;
z-index: 50;
}
.toast.is-show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ---- Responsive ---- */
@media (max-width: 520px) {
.page { padding: 28px 14px 56px; }
.page__head h1 { font-size: 23px; }
.controls { gap: 10px; }
.seg { width: 100%; flex-direction: column; align-items: flex-start; gap: 8px; }
.seg__btns { width: 100%; }
.seg__btn { flex: 1; text-align: center; padding: 8px 8px; }
.panel__body { min-height: 340px; padding: 32px 16px; }
.state__actions { width: 100%; }
.btn { flex: 1; justify-content: center; }
.errcode { flex-wrap: wrap; justify-content: center; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}(function () {
"use strict";
/* ---- Framing copy for each error variant ---- */
var FRAMES = {
generic: {
title: "Something went wrong",
desc: "We couldn’t load your revenue analytics. This is on our side — please try again in a moment.",
code: "ERR_500_GATEWAY · req_8f3a1c",
icon: "warn",
// generic 500s usually succeed on retry
failRetry: false
},
offline: {
title: "You appear to be offline",
desc: "We can’t reach the server right now. Check your connection and try again.",
code: "ERR_NETWORK · ECONNRESET",
icon: "wifi",
// first network retry stays failing to feel real
failRetry: true
},
notfound: {
title: "We couldn’t find that data",
desc: "This report may have been moved or deleted. Try reloading, or contact support if it should be here.",
code: "ERR_404_NOT_FOUND · revenue/30d",
icon: "search",
failRetry: false
}
};
var ICONS = {
warn:
'<svg class="art__svg" viewBox="0 0 120 120" fill="none"><circle cx="60" cy="60" r="54" class="art__ring"/><path class="art__tri" d="M60 28 L92 84 L28 84 Z"/><line class="art__bang" x1="60" y1="50" x2="60" y2="68"/><circle class="art__dot" cx="60" cy="76" r="2.6"/></svg>',
wifi:
'<svg class="art__svg" viewBox="0 0 120 120" fill="none"><circle cx="60" cy="60" r="54" class="art__ring"/><g stroke="var(--danger)" stroke-width="5" stroke-linecap="round" fill="none"><path d="M34 56 C50 42 70 42 86 56"/><path d="M44 68 C53 60 67 60 76 68"/></g><circle cx="60" cy="80" r="3.4" fill="var(--danger)"/><line x1="40" y1="40" x2="80" y2="80" stroke="var(--danger)" stroke-width="5" stroke-linecap="round"/></svg>',
search:
'<svg class="art__svg" viewBox="0 0 120 120" fill="none"><circle cx="60" cy="60" r="54" class="art__ring"/><circle cx="54" cy="54" r="18" stroke="var(--danger)" stroke-width="5" fill="rgba(212,80,62,0.08)"/><line x1="68" y1="68" x2="84" y2="84" stroke="var(--danger)" stroke-width="6" stroke-linecap="round"/></svg>'
};
var currentFrame = "generic";
var stage = document.querySelector(".stage");
var errState = document.querySelector('[data-state="error"]');
var okState = document.querySelector('[data-state="ok"]');
var artBox = errState.querySelector(".art");
var elTitle = document.getElementById("stTitle");
var elDesc = document.getElementById("stDesc");
var elCode = document.getElementById("stCode");
var retryBtn = document.getElementById("retryBtn");
var retryNote = document.getElementById("retryNote");
var copyBtn = document.getElementById("copyBtn");
var supportBtn = document.getElementById("supportBtn");
var breakBtn = document.getElementById("breakBtn");
var toastEl = document.getElementById("toast");
var retryCount = 0;
var toastTimer;
/* ---- Toast helper ---- */
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2400);
}
/* ---- Apply a framing variant ---- */
function applyFrame(key) {
var f = FRAMES[key];
if (!f) return;
currentFrame = key;
retryCount = 0;
artBox.innerHTML = ICONS[f.icon];
elTitle.textContent = f.title;
elDesc.textContent = f.desc;
elCode.textContent = f.code;
retryNote.textContent = "";
retryNote.classList.remove("is-error");
showError();
}
function showError() {
okState.hidden = true;
errState.hidden = false;
// re-trigger entrance animation
errState.style.animation = "none";
void errState.offsetWidth;
errState.style.animation = "";
}
function showSuccess() {
errState.hidden = true;
okState.hidden = false;
okState.style.animation = "none";
void okState.offsetWidth;
okState.style.animation = "";
}
/* ---- Retry flow: spinner -> resolve ---- */
function doRetry() {
if (retryBtn.classList.contains("is-loading")) return;
retryCount++;
var f = FRAMES[currentFrame];
retryBtn.classList.add("is-loading");
retryBtn.setAttribute("aria-busy", "true");
retryBtn.querySelector(".btn__txt").textContent = "Retrying…";
retryNote.textContent = "Reconnecting and reloading data…";
retryNote.classList.remove("is-error");
setTimeout(function () {
retryBtn.classList.remove("is-loading");
retryBtn.removeAttribute("aria-busy");
retryBtn.querySelector(".btn__txt").textContent = "Try again";
// network framing fails the first attempt, succeeds after
var willFail = f.failRetry && retryCount < 2;
if (willFail) {
retryNote.textContent = "Still can’t reach the server. Please check your connection.";
retryNote.classList.add("is-error");
toast("Retry failed — still offline");
} else {
retryNote.textContent = "";
toast("Data loaded successfully");
showSuccess();
}
}, 1500);
}
/* ---- Copy error code ---- */
function copyCode() {
var text = elCode.textContent.trim();
var done = function () { toast("Error code copied"); };
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text); done(); });
} else {
fallbackCopy(text);
done();
}
}
function fallbackCopy(text) {
var ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
try { document.execCommand("copy"); } catch (e) { /* noop */ }
document.body.removeChild(ta);
}
/* ---- Segmented control wiring ---- */
function wireSegments() {
document.querySelectorAll(".seg").forEach(function (group) {
var btns = group.querySelectorAll(".seg__btn");
btns.forEach(function (btn) {
btn.addEventListener("click", function () {
btns.forEach(function (b) {
b.classList.remove("is-active");
b.setAttribute("aria-pressed", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-pressed", "true");
if (btn.dataset.frame) {
applyFrame(btn.dataset.frame);
} else if (btn.dataset.layout) {
stage.setAttribute("data-layout", btn.dataset.layout);
}
});
});
});
}
/* ---- Events ---- */
retryBtn.addEventListener("click", doRetry);
copyBtn.addEventListener("click", copyCode);
breakBtn.addEventListener("click", function () {
applyFrame(currentFrame);
toast("Simulated a fresh failure");
});
supportBtn.addEventListener("click", function (e) {
e.preventDefault();
toast("Opening support — ref " + elCode.textContent.trim());
});
// Esc resets the demo back to the error state
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !okState.hidden) {
applyFrame(currentFrame);
}
});
wireSegments();
applyFrame("generic");
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Something went wrong — Error state</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="page">
<header class="page__head">
<span class="eyebrow">Empty states</span>
<h1>Something-went-wrong state</h1>
<p class="lede">A resilient error surface for failed data loads — with retry, a faux error code, and a switch between framings and layouts.</p>
</header>
<section class="controls" aria-label="Demo controls">
<div class="seg" role="group" aria-label="Error framing">
<span class="seg__label">Framing</span>
<div class="seg__btns">
<button type="button" class="seg__btn is-active" data-frame="generic" aria-pressed="true">Server 500</button>
<button type="button" class="seg__btn" data-frame="offline" aria-pressed="false">Network</button>
<button type="button" class="seg__btn" data-frame="notfound" aria-pressed="false">Not found</button>
</div>
</div>
<div class="seg" role="group" aria-label="Layout">
<span class="seg__label">Layout</span>
<div class="seg__btns">
<button type="button" class="seg__btn is-active" data-layout="inline" aria-pressed="true">Inline card</button>
<button type="button" class="seg__btn" data-layout="full" aria-pressed="false">Full page</button>
</div>
</div>
</section>
<section class="stage" data-layout="inline" aria-live="polite">
<!-- Faux dashboard chrome so the inline card sits in real context -->
<div class="panel">
<div class="panel__bar">
<div class="panel__title">
<span class="dot dot--a"></span>
<span>Revenue analytics</span>
</div>
<span class="panel__meta">Last 30 days</span>
</div>
<div class="panel__body" id="panelBody">
<!-- ERROR STATE -->
<div class="state state--error" data-state="error">
<div class="art" aria-hidden="true">
<svg class="art__svg" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="54" class="art__ring" />
<path class="art__tri" d="M60 28 L92 84 L28 84 Z" />
<line class="art__bang" x1="60" y1="50" x2="60" y2="68" />
<circle class="art__dot" cx="60" cy="76" r="2.6" />
</svg>
</div>
<h2 class="state__title" id="stTitle">Something went wrong</h2>
<p class="state__desc" id="stDesc">We couldn’t load your revenue analytics. This is on our side — please try again in a moment.</p>
<p class="errcode">
<span class="errcode__k">Error</span>
<code id="stCode">ERR_500_GATEWAY · req_8f3a1c</code>
<button type="button" class="copy" id="copyBtn" aria-label="Copy error code">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>
</button>
</p>
<div class="state__actions">
<button type="button" class="btn btn--primary" id="retryBtn">
<span class="btn__spin" aria-hidden="true"></span>
<svg class="btn__ic" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg>
<span class="btn__txt">Try again</span>
</button>
<a href="#support" class="btn btn--ghost" id="supportBtn">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
Contact support
</a>
</div>
<p class="retry-note" id="retryNote" aria-live="polite"></p>
</div>
<!-- SUCCESS STATE (revealed after a successful retry) -->
<div class="state state--ok" data-state="ok" hidden>
<div class="art art--ok" aria-hidden="true">
<svg class="art__svg" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="54" class="art__ring art__ring--ok" />
<path class="art__check" d="M40 62 L54 76 L82 46" />
</svg>
</div>
<h2 class="state__title">Back online</h2>
<p class="state__desc">Your revenue analytics loaded successfully. Everything looks healthy.</p>
<div class="success-chart" aria-hidden="true">
<span style="--h:38%"></span><span style="--h:62%"></span><span style="--h:50%"></span>
<span style="--h:78%"></span><span style="--h:66%"></span><span style="--h:92%"></span>
<span style="--h:74%"></span>
</div>
<button type="button" class="btn btn--ghost" id="breakBtn">Simulate failure again</button>
</div>
</div>
</div>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Something-went-wrong state
A generic error surface for when a data fetch fails. The card centers a warning illustration, a “Something went wrong” headline, a plain-language detail line, and a copyable faux error code chip (with a one-click copy button and toast). Two actions sit below: a primary Try again button and a secondary Contact support link. Pressing Try again swaps the button into a loading spinner, then either resolves the panel into a friendly success state — complete with an animated recovery chart — or, for the network framing, reports a failure on the first attempt before recovering.
Two segmented controls drive the live demo. The Framing switcher cycles a generic server 500, a network-offline state (distinct illustration and retry behavior), and a not-found framing, each with its own copy, icon, and error code. The Layout switcher toggles between an inline card embedded in faux dashboard chrome and a centered full-page treatment with a subtle radial backdrop.
Everything is keyboard-usable with visible focus rings, the dynamic region is
aria-live, and Escape resets a recovered panel back to its error state. The layout
holds together down to roughly 360px, honors prefers-reduced-motion, and ships with a
small reusable toast() helper — no frameworks, no external assets.