Le téléversement de fichier
L'input file natif donne déjà le clavier, le focus et l'état du champ. Ce qui manque presque toujours, ce sont les contraintes dites avant la sélection, et une confirmation quand le fichier est retenu. Démo, code, critères RGAA.
5 critères RGAA en jeu
Essayez-le
PDF ou JPEG, 2 Mo maximum. Une facture d'énergie ou un avis d'imposition de moins de trois mois.
Atteignez le champ à la tabulation, puis ouvrez le sélecteur avec
Entrée ou Espace : c'est le champ natif, pas un bouton qui l'imite. Choisissez un fichier trop lourd, ou d'un format non prévu, et regardez ce qui se passe **avant** toute validation. Puis retirez le fichier : le focus revient au champ, il ne retombe pas en haut de la page.Cette démo existe aussi en page autonome.
L’erreur courante
L’input jugé trop laid, masqué derrière un faux bouton.
<!-- À ne pas faire -->
<input type="file" id="justificatif" style="display: none" />
<div class="bouton" onclick="document.getElementById('justificatif').click()">
Choisir un fichier
</div>
<span id="nom-du-fichier"></span> Ce motif casse quatre choses d’un coup, et chacune suffirait.
display: none retire le champ de l’ordre de tabulation. Il ne s’agit pas
d’un champ mal étiqueté : il n’existe plus pour le clavier ni pour les
technologies d’assistance. Le seul chemin restant passe par le div.
Le div n’est pas un bouton. Il ne prend pas le focus, ne répond ni à
Le champ n’a plus d’étiquette. Le <label for> visait un champ désormais
invisible, et le texte du div n’est le nom accessible de rien.
Rien n’est annoncé après le choix. Le nom du fichier atterrit dans un <span> muet, mis à jour sans que personne en soit averti : le lecteur d’écran
reste sur ce qu’il disait avant.
Trois variantes plus discrètes, et tout aussi coûteuses :
- L’input masqué par une classe
sr-only, avec un<label>habillé en bouton. Le motif est bon — le label reçoit le clic, l’input reste focalisable — mais la prise de focus devient invisible : le champ est découpé hors de l’écran, et l’anneau de focus avec lui. Il faut redonner l’indication sur le label, en:focus-within. - Les contraintes seulement dans le message d’erreur. « Fichier trop volumineux » après trois minutes d’envoi sur une connexion lente, quand « 2 Mo maximum » aurait coûté une ligne avant.
acceptpris pour une validation. Il ne fait que préfiltrer la boîte de dialogue du système ; l’utilisateur peut basculer sur « tous les fichiers », et un dépôt par glisser l’ignore. La vérification reste à écrire.
Le code, pas à pas
1. Le champ natif, et son étiquette
<label for="justificatif">Justificatif de domicile</label>
<input id="justificatif" type="file" /> Deux lignes, et le navigateur donne le focus, l’ouverture au clavier, le nom du fichier affiché, et l’annonce de l’état du champ. Rien de tout cela n’est à réécrire, et tout se perd dès qu’on masque l’input.
L’étiquette doit nommer la pièce attendue, pas l’action. « Choisir un fichier » est déjà le texte que le navigateur affiche dans le champ ; le répéter en étiquette ne renseigne personne, tandis que « Justificatif de domicile » dit quoi préparer.
2. Les contraintes, dites avant — et reliées
<label for="justificatif">Justificatif de domicile</label>
<p id="justificatif-aide">
PDF ou JPEG, 2 Mo maximum. Une facture d'énergie de moins de trois mois.
</p>
<input id="justificatif" type="file" aria-describedby="justificatif-aide" /> C’est le point central de ce composant. Les formats acceptés sont une indication du type de données et/ou de format, et le test 11.10.5 les veut visibles avant la validation du formulaire — pas seulement dans l’erreur qui suit un échec. La taille limite ne relève d’aucun test du référentiel : la taire coûte pourtant le même téléversement raté, sur la connexion la plus lente.
aria-describedby les rattache au champ : la description est lue à la prise de
focus, au moment où l’utilisateur s’apprête à choisir, sans qu’il ait à quitter
le mode formulaire pour retrouver le paragraphe. Le test 11.10.5 ne l’exige
pas — une indication visible qui nomme le champ lui suffit. Mais posez aria-invalid="true" en cas de refus, et c’est le test 11.10.7 qui s’applique :
l’indication de format doit alors être dans l’étiquette ou dans le passage de
texte associé au champ. Le lien cesse d’être un confort.
3. accept filtre, il ne contrôle pas
<input id="justificatif" type="file" accept=".pdf,.jpg,.jpeg" /> accept améliore le confort : la boîte de dialogue du système propose d’abord
les fichiers plausibles. Il ne garantit rien — l’utilisateur peut élargir la
sélection, et un dépôt par glisser ne le consulte pas.
Vérifiez donc le fichier reçu, et vérifiez-le aussi côté serveur. Un accept seul est une commodité, pas un contrôle de saisie.
4. Confirmer le choix, refuser quand il faut
Choisir un fichier ne recharge pas la page et ne déplace pas le focus. Sans annonce explicite, rien ne dit que la sélection a été prise en compte.
<!-- Présentes dès le chargement, vides -->
<p id="justificatif-erreur" role="alert"></p>
<p role="status"></p> Le RGAA distingue les deux au critère 7.5 : role="status" pour la réussite ou
le résultat d’une action, role="alert" pour l’avertissement d’une erreur.
Ici, « facture-mars.pdf ajouté, 240 ko » va dans le premier, « ce fichier
pèse 4 Mo » dans le second.
Les deux régions doivent exister avant l’annonce, vides. Créées au moment où le message arrive, elles ne sont pas restituées par tous les lecteurs d’écran.
En cas de refus, le champ porte l’état, et la région d’erreur devient l’une de ses descriptions :
<input
id="justificatif"
type="file"
accept=".pdf,.jpg,.jpeg"
aria-describedby="justificatif-aide justificatif-erreur"
aria-invalid="true"
/> aria-describedby accepte plusieurs identifiants : l’aide et l’erreur sont
lues l’une après l’autre. aria-invalid dit l’état que la bordure rouge dit à
l’œil — et le symbole ⚠, masqué par aria-hidden, double la couleur sans
rien ajouter à l’annonce.
5. Retirer un fichier, sans perdre le focus
Un champ de fichier ne se vide pas au clavier : ni Suppr ni
Retour arrière n'y font rien. Sans bouton dédié, un fichier choisi par erreur ne se retire qu'en rechargeant la page.function retirer() {
champ.value = '';
// Le bouton disparaît avec le fichier qu'il vient de retirer : sans ce
// déplacement, le focus retombe sur le corps du document.
champ.focus();
} C’est le second volet du critère 7.3 : un script ne doit pas supprimer le focus d’un élément qui le reçoit. Supprimer l’élément focalisé revient au même.
6. Habiller le champ sans le casser
Si l’apparence par défaut ne convient pas, deux voies tiennent debout.
La plus simple : ::file-selector-button, qui style le bouton du champ natif
sans rien y changer d’autre. C’est ce que fait la démo.
L’autre : le <label> habillé en bouton, l’input rendu invisible mais pas
masqué.
/* L'input reste focalisable : c'est le label qui montre la prise de focus. */
.champ-fichier input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
.champ-fichier:focus-within label {
outline: 3px solid;
outline-offset: 2px;
} Ce qu’il ne faut pas faire, c’est display: none ou visibility: hidden : les
deux retirent le champ du clavier, et aucune couche de script ne le rend.
Au clavier
| Touche | Effet |
|---|---|
| Tab | Amène au champ. Le champ compte pour un seul arrêt, quel que soit son habillage. |
| Entrée / Espace | Ouvre la boîte de dialogue de choix de fichier du système. |
| Échap | Ferme cette boîte sans choisir. Elle appartient au système : ses touches ne sont pas les vôtres, et le focus revient au champ. |
| Suppr / Retour arrière | Rien. Un champ de fichier ne se vide pas au clavier — d’où le bouton « Retirer ». |
Ce que le lecteur d’écran restitue
À la prise de focus : le nom donné par l’étiquette, la nature du champ, son état, puis la description — de l’ordre de « Justificatif de domicile, bouton Choisir un fichier, aucun fichier sélectionné, PDF ou JPEG, 2 Mo maximum ». La nature annoncée varie beaucoup d’un lecteur à l’autre pour ce champ ; les quatre informations, elles, ne doivent pas varier.
Après un choix, la région de statut ajoute « facture-mars.pdf ajouté, 240 ko ». Après un refus, la région d’alerte interrompt avec le motif.
Si vous n’entendez que « bouton, Choisir un fichier », votre étiquette n’est
pas liée : ce que vous entendez est le texte du navigateur. Si vous n’entendez
pas les formats acceptés, l’aide existe mais aucun aria-describedby ne la
rattache. Et si rien ne se dit après le choix, il manque la région de statut —
c’est le défaut le plus fréquent de ce composant, parce qu’à la souris on voit
le nom du fichier s’inscrire et qu’on croit l’affaire close.
Checklist
- Un
<input type="file">réel et atteignable au clavier — jamaisdisplay: none. - Une
<label for>qui nomme la pièce attendue, pas l’action. - Formats visibles avant la sélection ; reliés par
aria-describedby— exigé dès que le champ portearia-invalid. acceptpour le confort, une vérification en script et côté serveur pour le contrôle.role="status"pour la confirmation,role="alert"pour le refus, présentes dès le premier rendu.- Un bouton « Retirer », et le focus reconduit au champ quand il disparaît.
Les critères RGAA en jeu
- Critère 11.1 — Étiquette de champ de formulaire
Un champ de fichier est un champ de formulaire comme un autre : il lui faut une étiquette liée. Le motif du bouton stylé qui masque l'input la fait disparaître, et « Parcourir » est le texte du navigateur, pas un nom que vous avez donné.
- Critère 11.2 — Pertinence de l’étiquette
« Fichier » ou « Choisir un fichier » ne dit pas quel document est attendu. L'étiquette doit nommer la pièce demandée — « Justificatif de domicile » — car c'est la seule chose qu'entendra qui ne voit pas le formulaire.
- Critère 11.10 — Contrôle de saisie
Les formats acceptés sont l'indication du type de données et/ou de format que le test 11.10.5 veut visible avant la validation, pas seulement dans le message d'erreur. Les découvrir après un téléversement raté, c'est les découvrir trop tard.
- Critère 7.5 — Messages de statut
Choisir un fichier ne recharge rien et ne déplace pas le focus : sans message de statut, personne n'apprend que la sélection a été prise en compte, ni sous quel nom.
- Critère 7.3 — Contrôle des scripts au clavier
Masquer l'input en display:none derrière un div cliquable retire le champ de l'ordre de tabulation. Et le bouton « Retirer » qui disparaît avec le fichier qu'il retire laisse le focus nulle part.
Vérifiez votre composant avec l'extension
Pour aller plus loin
- Les champs de formulaire : l’étiquette, l’aide et l’erreur liées, dans le cas général dont celui-ci est une variante.
- Critère 11.10 — Contrôle de saisie : le détail des sept tests, dont le 11.10.5 et le 11.10.7 cités plus haut.
- « Indication du type de données et/ou de format » au glossaire : la définition, limitative, qui dit ce que le 11.10.5 couvre et ce qu’il ne couvre pas.
- Critère 7.5 — Messages de statut : quand employer
status, quand employeralert. - « Message de statut » au glossaire : la définition qui décide de ce qui relève de ce critère.
Sources
- MDN —
<input type="file">— la référence de l’élément,acceptetFileListcompris - W3C WAI — Forms Tutorial — l’étiquetage et les instructions, cas par cas
- WCAG — Understanding 3.3.2 Labels or Instructions — pourquoi les contraintes se disent avant la saisie
- WCAG — Understanding 4.1.3 Status Messages — la règle derrière
role="status" - GOV.UK Design System — File upload — les règles d’usage, éprouvées à grande échelle