Connection Status Banner
When the connection drops, a red banner slides down saying you’re offline, with little dots while it tries to reconnect. When the connection comes back the banner turns green, swaps in a tick and “Back online”, then slides away. It follows the browser’s real online and offline events.
How to use it
- Paste the snippet just before
</head>. - Add
data-connection-status-bannerto<body>for a banner fixed to the top of the window, or to any box for a banner along the top of that box. - That’s it: it appears when the browser goes offline and clears when it’s back. While offline the element has the class
csb-offline, so you can dim or disable things with CSS.
Options
Add any of these attributes to the same element.
data-position="bottom" | Slide up from the bottom edge instead of down from the top. |
data-offline-text | Offline message. Default “You’re offline. Reconnecting”. |
data-online-text | Back-online message. Default “Back online”. |
data-offline-color | Offline colour. Default #dc2626. |
data-online-color | Back-online colour. Default #16a34a. |
data-hold | How long the green banner stays before sliding away. Default 1800 ms. |
data-autoplay | Drop and restore the connection by itself while in view, for demos. |
The banner is a polite live region, so screen readers announce each change. Call animacious.connectionStatus(el, false) or (el, true) to show it for your own reasons, say when your server stops answering. With reduced motion it appears and changes without sliding.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Connection Status Banner: when the connection drops, a banner slides
// down from the top of the page (or of any box you put it on) saying
// you're offline, with little dots while it tries to reconnect. When the
// connection is back the banner turns green, says so with a tick, and
// slides away a moment later. It follows the browser's real online and
// offline events.
// Usage: <body data-connection-status-banner> (fixed to the top of the window)
// or: <div data-connection-status-banner>…</div> (inside that box)
// Optional: data-position="bottom" (slide up from the bottom instead),
// data-offline-text (default "You're offline. Reconnecting"),
// data-online-text (default "Back online"), data-offline-color (default
// #dc2626), data-online-color (default #16a34a), data-hold (ms the green
// banner stays, default 1800), data-autoplay (drops and restores the
// connection by itself while in view; for demos).
// While offline the element has the class "csb-offline", so you can
// dim or disable things with CSS.
// Script use: animacious.connectionStatus(el, false) shows the offline
// banner, (el, true) the back-online one.
var DEFAULTS = {
offlineText: "You're offline. Reconnecting", onlineText: 'Back online',
offlineColor: '#dc2626', onlineColor: '#16a34a', hold: 1800
};
var WIFI_OFF = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
'<path d="M2 8.8a15 15 0 0 1 4.2-2.6M10 5.1A15 15 0 0 1 22 8.8M5 12.6a10 10 0 0 1 3.4-2M14.6 10.3A10 10 0 0 1 19 12.6M8.5 16.4a5 5 0 0 1 7 0"/>' +
'<circle cx="12" cy="20" r=".9" fill="currentColor"/><path d="M3 3l18 18"/></svg>';
var TICK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
var css =
'.csb-host{position:relative}' +
'.csb-clip{position:absolute;left:0;right:0;top:0;z-index:20;overflow:hidden;pointer-events:none;border-radius:inherit}' +
'.csb-bottom>.csb-clip{top:auto;bottom:0}' +
'.csb-fixed{position:fixed;z-index:9999;border-radius:0}' +
'.csb-bar{display:flex;align-items:center;justify-content:center;gap:.5em;padding:.65em 1em;font-size:.9em;font-weight:600;line-height:1.3;' +
'color:#fff;text-align:center;visibility:hidden;pointer-events:auto}' +
'.csb-msg{display:inline-flex;align-items:center;gap:.5em}' +
'.csb-msg svg{flex:none;width:1.15em;height:1.15em}' +
'.csb-dots{display:inline-flex;gap:.18em;margin-left:.1em}' +
'.csb-dots i{width:.28em;height:.28em;border-radius:50%;background:currentColor;animation:csb-dot 1.2s ease-in-out infinite}' +
'.csb-dots i:nth-child(2){animation-delay:.15s}.csb-dots i:nth-child(3){animation-delay:.3s}' +
'@keyframes csb-dot{0%,60%,100%{opacity:.35;transform:translateY(0)}30%{opacity:1;transform:translateY(-.18em)}}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset;
var bottom = d.position === 'bottom';
var clip = document.createElement('div');
clip.className = 'csb-clip';
if (el === document.body) clip.classList.add('csb-fixed');
else if (getComputedStyle(el).position === 'static') el.classList.add('csb-host');
if (bottom) el.classList.add('csb-bottom');
var bar = document.createElement('div');
bar.className = 'csb-bar';
bar.setAttribute('role', 'status');
bar.setAttribute('aria-live', 'polite');
clip.appendChild(bar);
if (el === document.body && bottom) { clip.style.top = 'auto'; clip.style.bottom = '0'; }
el.appendChild(clip);
el._csb = { bar: bar, bottom: bottom, shown: false, online: true, timer: null, auto: null };
}
function message(el, online) {
var d = el.dataset;
var span = document.createElement('span');
span.className = 'csb-msg';
span.innerHTML = (online ? TICK : WIFI_OFF) + '<span></span>' +
(online ? '' : '<span class="csb-dots" aria-hidden="true"><i></i><i></i><i></i></span>');
span.children[1].textContent = online ? (d.onlineText || DEFAULTS.onlineText) : (d.offlineText || DEFAULTS.offlineText);
return span;
}
function hide(el) {
var s = el._csb;
s.shown = false;
if (reduced()) { s.bar.style.visibility = 'hidden'; return; }
anime.animate(s.bar, {
y: s.bottom ? '100%' : '-100%', duration: 380, ease: 'in(2)',
onComplete: function () { if (!s.shown) s.bar.style.visibility = 'hidden'; }
});
}
function set(el, online) {
var s = el._csb, d = el.dataset;
if (online === s.online && (online || s.shown)) return;
s.online = online;
clearTimeout(s.timer);
el.classList.toggle('csb-offline', !online);
// Back online with no banner showing: nothing to say.
if (online && !s.shown) return;
var bar = s.bar;
var color = online ? (d.onlineColor || DEFAULTS.onlineColor) : (d.offlineColor || DEFAULTS.offlineColor);
var msg = message(el, online);
var old = bar.firstChild;
if (reduced()) {
bar.textContent = '';
bar.appendChild(msg);
bar.style.background = color;
bar.style.visibility = 'visible';
s.shown = true;
} else if (!s.shown) {
// Slide in.
bar.textContent = '';
bar.appendChild(msg);
bar.style.background = color;
bar.style.visibility = 'visible';
s.shown = true;
anime.animate(bar, { y: [s.bottom ? '100%' : '-100%', '0%'], duration: 650, ease: 'outBack(1.3)' });
anime.animate(msg, { opacity: [0, 1], duration: 400, delay: 200 });
} else {
// Change colour and swap the message.
anime.animate(bar, { backgroundColor: color, duration: 450, ease: 'out(2)' });
anime.animate(bar, { scaleY: [{ to: 1.08, duration: 140 }, { to: 1, duration: 420, ease: 'outElastic(1, .6)' }] });
bar.appendChild(msg);
msg.style.position = 'absolute';
if (old) anime.animate(old, { opacity: 0, y: '-0.6em', duration: 220, ease: 'in(2)', onComplete: function () { old.remove(); msg.style.position = ''; } });
else msg.style.position = '';
anime.animate(msg, { opacity: [0, 1], y: ['0.6em', '0em'], duration: 380, delay: 160, ease: 'out(3)' });
}
if (online) s.timer = setTimeout(function () { hide(el); }, +d.hold || DEFAULTS.hold);
}
function autoplay(el) {
var s = el._csb;
var steps = [[false, 1200], [true, 3000], [null, 3200]];
var i = 0, visible = false, timer = null;
function next() {
var st = steps[i++ % steps.length];
if (st[0] !== null) set(el, st[0]);
timer = setTimeout(next, steps[i % steps.length][1]);
}
new IntersectionObserver(function (entries) {
var v = entries[0].isIntersecting;
if (v === visible) return;
visible = v;
clearTimeout(timer);
if (v) timer = setTimeout(next, i === 0 ? 900 : 1200);
}, { threshold: 0.3 }).observe(el);
s.auto = true;
}
function init() {
document.querySelectorAll('[data-connection-status-banner]').forEach(function (el) {
if (el._csb) return;
setup(el);
if (!navigator.onLine && el.dataset.autoplay === undefined) set(el, false);
if (el.dataset.autoplay !== undefined && el.dataset.autoplay !== 'false') autoplay(el);
});
}
function all(online) {
document.querySelectorAll('[data-connection-status-banner]').forEach(function (el) {
if (el._csb && !el._csb.auto) set(el, online);
});
}
window.addEventListener('offline', function () { all(false); });
window.addEventListener('online', function () { all(true); });
window.animacious = window.animacious || {};
window.animacious.connectionStatus = function (el, online) { if (el._csb) set(el, online !== false); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><body data-connection-status-banner>
<!-- along the bottom of an app panel, with your own words: -->
<div class="panel" data-connection-status-banner data-position="bottom" data-offline-text="No connection. Changes will sync later">…</div>
<style>.csb-offline .save-button { opacity: .5; pointer-events: none; }</style>