Un script léger pour adapter l'expérience utilisateur selon l'état de la connexion internet. Affiche des alertes contextuelles et permet d'ajuster l'interface (animations, contenu) en fonction de la bande passante disponible.
Over-kill ? Un peu oui… mais ça reste utile. Et en plus, c'est accessible friendly.
À quoi ça sert ?
Le script surveille deux états critiques :
Hors connexion
Détecte la perte totale de connexion et affiche un bandeau d'alerte. Ajoute la classe .offline sur <html> pour adapter le CSS.
Connexion dégradée
Identifie les connexions lentes (2G, 3G, 4G lente) via l'API Network Information et une mesure de latence réelle. Affiche un bandeau informatif et gère .slow-connection sur <html>.
Exemples
Connexion lente sur navigateur Edge
Hors connexion sur iPhone
Fonctionnalités
- Mesure de la latence réelle (RTT) avec un fetch chronomètré ;
- Bandeaux accessibles avec
role="alert"etaria-live,assertivepour leofflineetpolitepour la dégradation réseau ; - Classe
.no-motionautomatique pour éventuellement désactiver les animations ; - Événement custom
network:motion-changepour intégration dans d'autre scripts ; - Respect du mode économie de données (
saveData) ; - Polling à peu près intelligent au chargement (2 secondes).
Cas d'usage pertinents
Progressive Web Apps
Essentielles pour gérer le mode offline et la synchronisation différée.
Applications métier
Dashboards temps réel, CRM, outils collaboratifs où la connexion est critique.
Sites riches en médias
Basculer vers des versions « light » (images compressées, ou masquées si juste décoratives, vidéos désactivées) si connexion faible.
Zones à connexion instable
Rural, pays en développement, utilisateurs mobiles en déplacement.
Javascript
Taille brute : 6,92 kilo-octets (~2,36 kilo-octets gzip)
/**
* Gestionnaire d'état de connexion réseau et qualité
*
* Affiche des bandeaux d'alerte selon l'état de la connexion
* – Pas de connexion (offline).
* - Connexion lente (slow-2g, 2g, 3g, 4g faible)
*
* @module NetworkStatusBanner
* @requires i18n - Objet de traduction pour les messages
* @requires Element.prototype.setAttributes - Méthode custom pour définir plusieurs attributs
*
* @example
* // Le script s'initialise automatiquement au chargement du DOM
* // CSS requis :
* // :root.offline { ... }
* // #network-banner-offline { ... }
* // #network-banner-slow { ... }
*/
(() => {
/**
* Colle plusieurs attributs sur un élément en une fois
*
* @function Element#setAttributes
* @param {Object.<string, string|null>} object - Tes attributs en vrac (null = ignoré)
* @returns {void} Le néant. L'abysse. Tu connais la chanson.
*
*/
Element.prototype.setAttributes ??= function (object) {
Object.keys(object).forEach((property) => {
object[property] !== null && this.setAttribute(property, object[property]);
});
};
/**
* Émet un événement custom quand no-motion change
* @function emitNoMotionEvent
* @private
*/
function emitNoMotionEvent() {
const hasNoMotion = document.documentElement.classList.contains('no-motion');
window.dispatchEvent(new CustomEvent('network:motion-change', {
detail: {noMotion: hasNoMotion}
}));
}
/** @type {object|null} API Network Information si disponible */
const connection = navigator.connection;
/** @type {Object.<string, string>} Messages traduits pour chaque état */
const wordings = {
offline: '',
slow: ''
};
/** @type {Object.<string, HTMLDivElement>} Éléments DOM des bandeaux */
const banners = {
offline: null,
slow: null
};
/** @type {number|null} RTT mesuré manuellement (seulement si connection disponible) */
let measuredRTT = null;
/**
* Initialise les bandeaux de statut réseau
* Crée les éléments DOM et les prépend au body
*
* @function init
* @private
*/
function init() {
// Bandeau offline
banners.offline = document.createElement('div');
banners.offline.setAttributes({
id: 'network-banner-offline',
role: 'alert',
'data-nosnippet': ''
});
document.body.prepend(banners.offline);
// Bandeau connexion lente (seulement si l'API est disponible)
if (connection) {
banners.slow = document.createElement('div');
banners.slow.setAttributes({
id: 'network-banner-slow',
role: 'status',
'data-nosnippet': ''
});
document.body.prepend(banners.slow);
wordings.slow = i18n?.['slow-connection'] || 'Connexion ralentie…';
}
// Chargement des wordings i18n
wordings.offline = i18n?.offline || 'Pas de connexion réseau…';
}
/**
* Détermine si la connexion est considérée comme lente
*
* @function isSlowConnection
* @private
* @returns {boolean} true si connexion lente détectée
*/
function isSlowConnection() {
if (!connection) return false;
const {effectiveType, downlink, rtt, saveData} = connection;
const actualRTT = measuredRTT || rtt;
// Critères de connexion lente
return (
effectiveType === 'slow-2g' ||
effectiveType === '2g' ||
effectiveType === '3g' ||
(effectiveType === '4g' && (downlink < 1.5 || actualRTT > 400)) ||
actualRTT > 400 ||
saveData === true
);
}
/**
* Met à jour l'état visuel de la page selon la connexion réseau
*
* - Ajoute/retire les classes 'offline' et 'slow-connection' sur <html>
* - Affiche/masque les messages des bandeaux
* - Ajuste aria-live pour l'accessibilité
* - Préserve la classe 'no-motion' si activée manuellement via le menu d'options
* (clé 'a11yOptions' dans localStorage, indépendante de ce module)
* Si le menu d'options n'est pas présent, la valeur est simplement false
*
* @function updateNetworkStatus
* @private
*/
function updateNetworkStatus() {
const online = navigator.onLine;
// Gestion bandeau offline
banners.offline.setAttribute('aria-live', online ? 'off' : 'assertive');
banners.offline.textContent = online ? '' : wordings.offline;
document.documentElement.classList.toggle('offline', !online);
// Préserve no-motion si activé manuellement via le menu d'options (a11yOptions peut être absent)
const userNoMotion = !!JSON.parse(localStorage.getItem('a11yOptions') || '{}')?.options?.['no-motion'];
// Gestion connexion lente (seulement si l'API est disponible)
if (connection) {
const slow = online && isSlowConnection();
banners.slow.setAttribute('aria-live', slow ? 'polite' : 'off');
banners.slow.textContent = slow ? wordings.slow : '';
document.documentElement.classList.toggle('slow-connection', slow);
document.documentElement.classList.toggle('no-motion', !online || slow || userNoMotion);
emitNoMotionEvent();
} else {
document.documentElement.classList.toggle('no-motion', !online || userNoMotion);
emitNoMotionEvent();
}
}
/**
* Mesure la latence réelle avec un fetch chronométré
* Stocke le résultat dans measuredRTT
*
* @function measureNetworkLatency
* @private
* @returns {Promise<void>}
*/
async function measureNetworkLatency() {
const start = performance.now();
try {
await fetch('/blank.gif?t=' + Date.now(), {cache: 'no-cache'});
measuredRTT = performance.now() - start;
} catch (e) {
measuredRTT = null;
}
}
/**
* Gestionnaire de changement de connexion
* Re-mesure la latence puis met à jour le statut
*
* @function handleConnectionChange
* @private
*/
async function handleConnectionChange() {
if (connection) await measureNetworkLatency();
updateNetworkStatus();
}
// Écoute les changements d'état réseau
window.addEventListener('offline', updateNetworkStatus);
window.addEventListener('online', handleConnectionChange);
// Écoute les changements de qualité de connexion (seulement si API disponible)
connection?.addEventListener('change', handleConnectionChange);
// Initialisation au chargement du DOM
window.addEventListener('DOMContentLoaded', async () => {
init();
// Détection de connexion lente (seulement si API disponible)
if (connection) {
await measureNetworkLatency();
updateNetworkStatus();
// Polling avec mesure périodique pour détection initiale
const pollInterval = setInterval(async () => {
await measureNetworkLatency();
updateNetworkStatus();
}, 1000);
setTimeout(() => clearInterval(pollInterval), 2000);
} else {
// Juste la détection offline si pas d'API
updateNetworkStatus();
}
});
})();
Intégration avec d'autres scripts
L'événement network:motion-change permet de réagir aux changements de connexion dans d'autres scripts.
Exemple : pause automatique des vidéos
window.addEventListener('network:motion-change', (e) => {
if (e.detail.noMotion) {
// Pause toutes les vidéos en lecture
document.querySelectorAll('video').forEach(video => video.pause());
}
});
Le script s'initialise automatiquement au chargement du DOM. Tant qu'à faire, dans le head des pages, histoire de prévenir le navigateur de ce qui va se passer.
Il requiert :
- Un objet
i18npour les messages traduits (optionnel, fallback en français) ; - Un fichier
/blank.gifléger pour la mesure de latence ; - Des styles CSS pour les classes
.offline,.slow-connection.
:root.offline {
/* Styles dégradés */
}
#network-banner-offline,
#network-banner-slow {
/* Styles des bandeaux d'alerte */
}
Les styles CSS
Pour gagner du temps, sur Kortic, on a ça. En Nesting natif.
Particularité pour le offline, le contenu de la page passe en mode inerte.
Taille brute : 4,16 kilo-octets (~1,80 kilo-octets gzip)
[id*="network-banner"] {
display: none;
font-size: smaller;
margin: 0 var(--margin-hero);
padding: 0.3125rem 0;
line-height: 1.5rem;
text-align: center;
z-index: 6;
position: absolute;
top: 0;
width: 0;
height: 0;
--connection-off: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M30.35,9.55c-3.59-3.32-7.99-5.17-12.48-5.57l-.12,4.05c3.6.37,7.11,1.88,9.99,4.54.37.34.84.51,1.3.51.54,0,1.07-.23,1.46-.67.72-.83.65-2.11-.15-2.85ZM1.65,9.55c-.81.74-.87,2.02-.15,2.85.72.83,1.96.9,2.76.16,2.89-2.66,6.4-4.17,10.01-4.54l-.12-4.05c-4.5.4-8.9,2.26-12.5,5.58ZM16.15,21.06h-.29c-.68,0-1.23-.54-1.25-1.22l-.07-2.34c-1.64.29-3.21,1.07-4.48,2.35-.77.78-.78,2.06-.03,2.86.76.8,2,.81,2.77.03,1.76-1.78,4.63-1.78,6.39,0,.38.39.88.58,1.37.58.51,0,1.01-.2,1.4-.61.76-.8.74-2.07-.03-2.86-1.26-1.28-2.83-2.05-4.46-2.35l-.07,2.34c-.02.68-.57,1.22-1.25,1.22ZM23.76,17.93c.38.39.88.58,1.37.58s1.01-.2,1.4-.61c.76-.8.74-2.07-.03-2.85-2.47-2.5-5.6-3.92-8.83-4.29l-.12,4.05c2.27.33,4.46,1.36,6.21,3.12ZM5.5,15.04c-.77.78-.78,2.06-.03,2.85.76.8,2,.81,2.77.03,1.75-1.77,3.95-2.8,6.23-3.13l-.12-4.05c-3.23.36-6.37,1.79-8.85,4.29Z" opacity=".6"/><path d="M17.86,4.05l-.12,4.43-.08,2.99-.12,4.43-.08,2.95-.07,2.56c-.02.74-.57,1.33-1.25,1.33h-.29c-.68,0-1.23-.59-1.25-1.33l-.07-2.56-.08-2.95-.12-4.43-.08-2.99-.12-4.43-.02-.9c-.02-.77.55-1.41,1.25-1.41h1.26c.71,0,1.27.64,1.25,1.41l-.02.9ZM16,23.31h-.01c-1.08,0-1.95.9-1.95,2.02s.88,2.02,1.96,2.02,1.96-.9,1.96-2.02-.88-2.02-1.96-2.02Z"/></svg>');
--connection-slow: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 16"><path d="M12.12,6.85c2.42,0,4.44-.83,6.02-2.51-1.62-1.99-3.33-2.92-5.96-2.93-2.62,0-4.33.81-6.03,2.96,1.61,1.69,3.71,2.49,5.97,2.48ZM22.83,10.53c1.04-.39,1.78-1.28,3.52-1.28,2.2,0,3.65-.76,3.65-2.01,0-2.89-1.54-4.86-3.83-4.86-1.98,0-3.19,1.2-3.69,3.12-.35,1.26-.85,1.82-1.44,2.2.41.59,1.11,1.82,1.8,2.83ZM17.57,11.55c.77-.19,1.45-.3,2.44-.3.81,0,1.6-.13,1.98-.35-.94-1.21-2.59-4.79-3.46-6.15-.88.91-2.29,1.79-3.25,2.17.14,1.53.99,3.3,2.29,4.62ZM6.69,11.59c1.27-1.22,2.29-3.15,2.45-4.64-1.13-.42-2.51-1.24-3.33-2.11-.4.62-.81,1.29-1.19,2.04-.79,1.58-1.67,2.31-2.26,2.85-.22.21-.36.46-.36.79,0,.49.52.76,1.44.76h1.01c.95,0,1.6.12,2.23.31ZM26.34,6.4c-.42,0-.77-.34-.77-.76s.35-.76.77-.76.75.35.75.76-.34.76-.75.76ZM12.14,13.54c1.94,0,3.3-1.02,4.44-1.59-1.29-1.23-2.12-3.05-2.26-4.72-.7.2-1.41.29-2.14.29-.73,0-1.44-.09-2.13-.27-.14,1.56-.98,3.45-2.27,4.74,1.09.43,2.42,1.54,4.35,1.54ZM4.36,14.98c1.47,0,2.96-.65,3.68-1.65-.84-.55-1.88-1.07-2.87-1.11-.46.12-.95.14-1.46.25-.73.14-1.39.43-1.39,1.14,0,.89.88,1.37,2.04,1.37ZM19.64,15c1.16,0,2.04-.48,2.04-1.37,0-.7-.66-1.25-1.38-1.31-.52-.03-.94-.03-1.47-.07-.96.05-2.01.57-2.86,1.1.71,1,2.21,1.65,3.67,1.65Z"/></svg>');
}
#network-banner-offline,
#network-banner-slow {
&:before {
content: '';
display: inline-block;
background-color: currentColor;
-webkit-mask-size: contain;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
margin-right: 1rem;
vertical-align: middle;
}
}
:root.offline #network-banner-offline {
background-color: var(--orange, orange);
font-weight: bold;
color: white;
display: block;
position: sticky;
width: auto;
height: auto;
&:before {
-webkit-mask-image: var(--connection-off);
mask-image: var(--connection-off);
width: 1.5rem;
height: 1.5rem;
}
}
:root.slow-connection #network-banner-slow {
display: block;
position: static;
width: auto;
height: auto;
background-color: var(--basic-color-reverse, black);
color: var(--basic-color, white);
&:before {
-webkit-mask-image: var(--connection-slow);
mask-image: var(--connection-slow);
width: 1.25rem;
height: 0.75rem;
background-color: currentColor;
}
}
:root.offline {
overflow: hidden;
& body > :not(#network-banner-offline) {
pointer-events: none;
opacity: .5;
filter: grayscale(1);
}
}
/* si besoin…
:root.slow-connection {
& body > :not(#network-banner-slow) {
}
}
*/
Télécharger kortic-network.css
Compatibilité navigateurs
| Fonctionnalité | Chromium | Firefox | Safari | Edge | Opera | Support |
|---|---|---|---|---|---|---|
| Online/Offline | Oui (1+) | Oui (1+) | Oui (3+) | Oui (12+) | Oui (10+) | Universel |
| Network Information API | Oui (61+) | Non | Non | Oui (79+) | Oui (48+) | Limité |
performance.now() |
Oui (24+) | Oui (15+) | Oui (8+) | Oui (12+) | Oui (15+) | Mesure RTT manuelle |
| Custom Events | Oui (15+) | Oui (11+) | Oui (6+) | Oui (12+) | Oui (15+) | Support complet |
Dégradation gracieuse : Sur Firefox et Safari, seule la détection hors connexion fonctionne. La détection de connexion lente n'est pas disponible (API manquante), mais le script continue de fonctionner sans erreur.
Limitations
API Network Information
Disponible uniquement sur les navigateurs basés sur Chromium (Chrome, Edge, Opera, Brave...). Firefox et Safari ne l'implémentent pas. Le script se contente de la détection offline basique.
Précision RTT
La mesure manuelle dépend de la latence serveur et peut varier. Elle complète l'API native mais ne la remplace pas totalement.
Overhead
Le polling initial (2 secondes) génère des requêtes réseau. À réserver aux contextes où la détection précoce est critique.
Conclusion
En pratique : testez-le en mode avion ou en bridant votre connexion dans les DevTools. Vous verrez immédiatement l'intérêt.
Indispensable pour : PWA, applications métier, sites à fort trafic mobile
Gadget pour : Sites vitrine, blogs, portfolios (on l'a quand même mis ici…)
Un outil de niche qui prend tout son sens dans les environnements contraints ou les applications critiques. Pour un site classique, le navigateur gère déjà l'essentiel nativement, mais ne vous prévient pas.
That' all, Folks !