Upload progressif de fichiers : guide UX et technique
Créez des expériences d'upload progressif avec glisser-déposer, barres de progression et gestion élégante des erreurs pour une meilleure UX.
Un upload de fichier progressif donne aux utilisateurs un retour immédiat et fiable à chaque étape : à l'instant où ils déposent un fichier, une confirmation de sélection ; pendant l'upload, une barre de progression fluide avec un ETA réaliste ; en cas d'échec, des options de réessai spécifiques ; après la complétion, un état de succès clair avec les actions suivantes. Les ingrédients techniques sont le glisser-déposer via l'API HTML5 DataTransfer, les uploads chunkés avec reprenabilité, le streaming fetch avec ReadableStream pour la progression au bit près, et la gestion d'état qui survit aux rechargements d'onglet via IndexedDB. Bien fait, un utilisateur uploadant un fichier de 5 Go ne se demande jamais si l'application est gelée — ce qui se traduit directement par un taux d'abandon réduit et une meilleure conformité aux attentes UX des services grand public.
Ce que "progressif" signifie vraiment ici
Progressif a deux significations dans ce contexte. La première : l'amélioration progressive, pour que l'upload fonctionne comme un simple <input type="file"> POST sur un navigateur de 2012, et gagne le glisser-déposer, le chunking et les réessais quand JavaScript est disponible. La deuxième : la divulgation progressive, où l'interface révèle la complexité seulement quand nécessaire — montrez un pourcentage pendant l'upload, mais montrez les détails de réessai seulement en cas d'échec. Les deux significations pointent vers le même principe : l'utilisateur ne devrait jamais se retrouver dans une impasse, et ne jamais attendre sans information.
L'état d'échec à éviter est le « spinner de mort » — une animation de chargement générique qui ne donne aucune indication de progression, d'ETA, ou si quelque chose a mal tourné. Les utilisateurs annulent les uploads auxquels ils ne font pas confiance.
Glisser-déposer qui ne combat pas le navigateur
L'API HTML5 de glisser-déposer est tristement célèbre pour ses bugs. Quelques règles qui la rendent tolérable :
const dropzone = document.querySelector('.dropzone');
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('dragging');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('dragging');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('dragging');
handleFiles([...e.dataTransfer.files]);
});
Appelez preventDefault sur dragover sinon la cible de dépôt n'acceptera pas le drop. Utilisez e.dataTransfer.items au lieu de files si vous devez accepter des dossiers via webkitGetAsEntry() — c'est la seule façon de capturer récursivement le contenu des répertoires sur Chrome et Firefox.
Rendez aussi le repli utilisable : un <label> visible enveloppant un <input type="file" multiple> stylisé fonctionne pour 100 % des utilisateurs, y compris la navigation clavier et lecteur d'écran.
Montrer la progression en laquelle les utilisateurs ont confiance
Les barres de progression sautent pour trois raisons : des tailles de chunks inégales, le TCP slow-start, et la mise en tampon dans la pile réseau. Lissez-les avec une moyenne mobile sur 2 secondes :
const samples = []; // [{ time, bytes }]
function recordSample(bytes) {
const now = performance.now();
samples.push({ time: now, bytes });
while (samples.length > 1 && now - samples[0].time > 2000) samples.shift();
}
function throughput() {
if (samples.length < 2) return 0;
const delta = samples[samples.length - 1];
const base = samples[0];
return (delta.bytes - base.bytes) / ((delta.time - base.time) / 1000);
}
Calculez l'ETA comme (totalBytes - uploadedBytes) / throughput(), clampez l'affichage à au moins 5 secondes, et formatez-le en termes humains : « environ 2 minutes » pas « 124,3 secondes ». Montrez à la fois un pourcentage et un compteur d'octets (« 340 Mo sur 2,1 Go ») — les utilisateurs vérifient les deux quand quelque chose semble anormal.
États d'erreur avec récupération actionnable
Les messages génériques « l'upload a échoué » détruisent la confiance. Classifiez les échecs en cinq catégories et affichez chacun distinctement :
- Coupure réseau (événement offline, reset TCP) : « Reconnexion en cours... » avec réessai automatique
- Serveur 5xx : « Erreur serveur, réessai dans 5s » avec bouton de réessai manuel
- Serveur 4xx (413 trop grand, 415 type incorrect) : « Fichier rejeté : trop volumineux » avec remplacement de fichier
- Auth expirée (401, 403) : « Session expirée, connectez-vous pour continuer »
- Crash client (erreur JS, onglet tué par le navigateur) : Récupération depuis IndexedDB au rechargement
Associez le message à l'action unique qui le résout. Si l'utilisateur est hors ligne, montrez l'état en ligne/hors ligne surveillé via navigator.onLine et l'événement online.
Suivre les octets avec les streams Fetch
XMLHttpRequest.upload.onprogress a été la méthode traditionnelle pour suivre la progression des uploads, mais elle est peu fiable sur HTTP/3 et manque des octets mis en file d'attente dans le tampon d'envoi. L'approche moderne utilise ReadableStream pour compter les octets au fur et à mesure de leur production :
function trackedStream(blob, onBytes) {
let sent = 0;
return new ReadableStream({
async pull(controller) {
const reader = blob.stream().getReader();
while (true) {
const { done, value } = await reader.read();
if (done) { controller.close(); return; }
sent += value.byteLength;
onBytes(sent);
controller.enqueue(value);
}
}
});
}
Passez le stream comme body à fetch avec duplex: 'half'. La prise en charge Safari des streams de requête est arrivée en 17.4 ; avant ça, repliez sur XMLHttpRequest. Cela vous donne une progression précise à la milliseconde liée aux octets réels remis à la pile réseau.
Pause, reprise et annulation
Les utilisateurs attendent un bouton de pause sur tout ce qui prend plus d'une minute. Avec les uploads chunkés, la pause est juste « arrêter de dispatcher de nouveaux chunks », et la reprise reprend là où la file de travail s'est arrêtée. L'annulation utilise AbortController :
const ctrl = new AbortController();
cancelButton.onclick = () => ctrl.abort();
await fetch(url, { method: 'PUT', body: blob, signal: ctrl.signal });
À l'abandon, nettoyez : DELETE la session d'upload sur le serveur pour que le stockage ne fuite pas, videz l'entrée IndexedDB, retournez à l'état initial. La pause devrait préserver l'état ; l'annulation devrait le détruire. Rendez la distinction visible dans l'interface.
Survivre aux rechargements d'onglet
Persistez l'état d'upload après chaque chunk réussi :
await idb.put('uploads', {
sessionId, fileFingerprint, fileName, fileSize,
completedChunks: [...done], updatedAt: Date.now()
}, sessionId);
L'empreinte est un SHA-256 du premier 1 Mo du fichier plus la taille et lastModified — suffisant pour ré-identifier le fichier quand l'utilisateur le choisit à nouveau après rechargement. Au chargement de page, vérifiez IndexedDB pour des sessions de moins d'une heure et proposez la reprise : « Vous avez un upload en cours depuis il y a 12 minutes. Reprendre ? » N'auto-reprenez pas sans consentement — les utilisateurs rechargent parfois spécifiquement pour annuler.
Interactions accessibles et compatibles clavier
Une zone de dépôt qui ne répond qu'aux glissés de souris échoue les utilisateurs lecteur d'écran et clavier. Ajoutez :
role="button"ettabindex="0"sur la zone de dépôt- Gestionnaire de touches Entrée/Espace qui clique sur l'input fichier
aria-live="polite"sur la région de progression pour que les lecteurs d'écran annoncent les jalons- Styles de focus visibles, pas seulement au survol
- Des labels clairs — « Uploader un fichier » bat « Parcourir » qui bat une icône seule
Les tests clavier sont rapides : débranchez votre souris pendant 10 minutes et essayez de compléter un upload. Si vous ne pouvez pas, une partie de vos utilisateurs non plus.
HexaTransfer utilise exactement ce schéma progressif — repli formulaire simple, amélioration par glisser-déposer, streaming fetch, reprise IndexedDB, et récupération d'erreur spécifique.
Les détails que les utilisateurs remarquent vraiment
Le soin qui distingue les uploaders mémorables des grands vit dans les petits moments : une animation de dépôt qui confirme que le fichier a été capturé, une barre de progression qui se remplit doucement au lieu de sauter, un ETA qui devient plus précis avec le temps au lieu de fluctuer sauvagement, des messages d'erreur spécifiques qui vous disent quoi faire ensuite, une invite de reprise après un rechargement accidentel, un état de complétion qui persiste assez longtemps pour copier le lien de partage, et un comportement d'annulation qui arrête réellement l'upload immédiatement. Chacun de ces éléments représente quelques lignes de code. Livrez-les tous et votre uploader semble infiniment meilleur que le traitement par défaut de <input type="file">.
Essayez-le sur https://hexatransfer.com — gratuit, sans compte, 10 Go maximum.
Envoyez vos fichiers volumineux en toute sécurité avec le chiffrement de bout en bout
Transférez des fichiers jusqu'à 10 Go gratuitement avec le chiffrement de bout en bout. Aucun compte requis. Vos fichiers sont chiffrés dans votre navigateur avant l'envoi — personne d'autre ne peut les lire.
Envoyer un fichier