Formulaires et messages d'erreurs accessibles
Conception et accessibilité des formulaires web, avec prise en charge des erreurs de saisie.
Les formulaires sont quasiment toujours présents sur un site internet.
Le concept est simple mais demande quelques précautions pour les rendre simples d'utilisation et conformes en accessibilité.
Il faut prendre en compte la présentation dans un premier temps, mais également la gestion des erreurs qui ne manqueront pas d'être affichées.
La structure d'un formulaire
Les formulaires sont conçus avec différents types d'éléments :
- Des champs de saisie « simples » nom, prénom, adresse couriel, etc. ;
- Des cases à cocher, boutons radio ;
- Des listes déroulantes ;
- Des champs de saisie de texte de type textarea ;
- Et bien entendu des boutons, dont celui de validation ;
Les règles de base à respecter
- Chaque champ de formulaire doit être relié à une étiquette (label), cette étiquette doit être pertinente et elle doit être placée à proximité immédiate du champ ;
-
Préciser le format des données attendues (date, email, etc.), soit dans le label, soit dans un élément à
proximité immédiate du champ
et correctement relié au champ.
Attention pour les dates : lorsque vous fournissez un format du type jj/mm/aaaa, la vocalisation va donner quelque chose comme « gigi barre oblique aime aime barre oblique ah ah ah ah ». C'est rigolo mais il vaut mieux donner une date réelle, du jour ou ultérieure. - L'attribut placholder n'est pas une étiquette ! En plus d'un contraste le plus souvent insuffisant, l'information de la donnée attendue disparait lors de la saisie ;
- L'attribut HTML type pour les champs où les utilisateurs doivent respecter un format de données est fortement recommandé. En plus de préciser le format attendu, il permet d'afficher le clavier correspondant sur les mobiles ;
-
Utiliser l'attribut HTML autocomplete : pourquoi ? Parce que !
Lire la documentation sur autocomplete, ce sera plus simple. -
Utiliser des fieldset pour regrouper les champs de même nature. Par exemple :
- Champs successifs pour saisir une date (jour/mois/année) ;
- Champs successifs pour un numéro de téléphone ;
- Données de carte bancaire ;
- Un ensemble de boutons radio ou de cases à cocher qui se rapportent à une question.
-
On essaie aussi d'oublier les CAPTCHAs, ce n'est pas ce qu'il y a de plus gentil pour l'accessibilité.
Pour la sécurité il y a des alternatives, comme le Honeypot.
A propos des champs obligatoires
Un formulaire est conçu pour envoyer des informations. En partant de là, il est rare qu'aucun champ ne soit nécessaire. La question se pose donc de savoir comment préciser quels champs sont nécessaires et lesquels sont facultatifs.
Il y a deux écoles sur le sujet.
La plus connue consiste à placer une astérisque ٭ à la suite du texte
de chaque étiquette. Le problème de cette méthode est qu'il faut parvenir à vocaliser cet astérisque pour lui
donner un sens avec un lecteur d'écran. Il faut également prévenir les utilisateurs de la signification de cet
astérisque avec un texte placé avant les formulaires. Après le formulaire,
c'est un peu trop tard…
C'est (hélas) ce qu'on retrouve le plus souvent, mais sans mentir, c'est très daté 1999. Et si ça fonctionnait,
nous n'aurions pas besoin de gérer les erreurs. Ce qu'il faut dire.
Et considérant que les plus grands acteurs d'internet ont choisi cette option, on peut imaginer qu'ils ont réalisé les études nécessaires concernant l'expérience utilisateur.
Un peu de lecture sur le sujet avec ces articles.
- Why users fill out less if you mark required fields ;
- Form fields — Required vs optional ;
- Mandatory versus optional fields.
Bien évidemment, on trouvera aussi des articles disant le contraire, mais comme je suis d'une mauvaise foi sans faille, je n'en parlerai pas ici. Non mais !
Comment gérer les messages d'erreurs ?
Voici un point important et souvent oublié : comment rendre des messages d'erreurs « sympathiques » pour tout le monde ? Particulièrement pour les technologies d'assistance.
Imaginons un formulaire très simple avec trois champs de saisie. Deux sont obligatoires et un est optionnel, associé à une liste.
Ce qui donne un formulaire comme ça :
Code HTML du formulaire
<form action="#" id="form-sample" novalidate autocomplete="off" class="outer-line box-shadow-thin" spellcheck="false">
 <fieldset class="flex small-gutter">
 <legend class="h4">Vos coordonnées</legend>
 <span class="clip" id="messages-slot"></span>
 <div class="s-6 hidden-describe-input md-label">
 <label for="form-sample-sender">Votre nom</label>
 <input enterkeyhint="next" type="text" autocomplete="family-name" id="form-sample-sender" aria-describedby="form-sample-sender-desc" aria-required="true">
 <p class="describe-input" id="form-sample-sender-desc">Saisir votre nom de famille </p>
 </div>
 <div class="s-6 md-label">
 <label for="form-sample-mail">Adresse courriel</label>
 <input enterkeyhint="next" type="email" autocomplete="email" id="form-sample-mail" aria-describedby="form-sample-mail-desc" aria-required="true">
 <p class="describe-input" id="form-sample-mail-desc">Saisir un courriel valide </p>
 </div>
 <fieldset class="flex small-gutter justify-content-stretch s-12">
 <legend class="mb-0 default-headers">Téléphone</legend>
 <div class="s-12 m-6 hidden-describe-input md-label">
 <label for="phone-prefix">Indicatif</label>
 <div>
 <select enterkeyhint="next" id="phone-prefix" autocomplete="off" data-rel="form-sample-phone" aria-describedby="form-sample-phone-prefix-desc">
 <option value="">Sélectionner un pays</option>
 <option value="AF-AFG-004">Afghanistan (l') (+93)</option>
 <option value="AL-ALB-008">Albanie (l') (+355)</option>
 <option value="DZ-DZA-012">Algérie (l') (+213)</option>
 <option value="AS-ASM-016">Samoa américaines (les) (+1-684)</option>
 <option value="AD-AND-020">Andorre (l') (+376)</option>
 <option value="AO-AGO-024">Angola (l') (+244)</option>
 <option value="AI-AIA-660">Anguilla (+1-264)</option>
 <option value="AQ-ATA-010">Antarctique (l') (+672)</option>
 <option value="AG-ATG-028">Antigua-et-Barbuda (+1-268)</option>
 <option value="AR-ARG-032">Argentine (l') (+54)</option>
 <option value="AM-ARM-051">Arménie (l') (+374)</option>
 <option value="AW-ABW-533">Aruba (+297)</option>
 <option value="AU-AUS-036">Australie (l') (+61)</option>
 <option value="AT-AUT-040">Autriche (l') (+43)</option>
 <option value="AZ-AZE-031">Azerbaïdjan (l') (+994)</option>
 <option value="BS-BHS-044">Bahamas (les) (+1-242)</option>
 <option value="BH-BHR-048">Bahreïn (+973)</option>
 <option value="BD-BGD-050">Bangladesh (le) (+880)</option>
 <option value="BB-BRB-052">Barbade (la) (+1-246)</option>
 <option value="BY-BLR-112">Bélarus (le) (+375)</option>
 <option value="BE-BEL-056">Belgique (la) (+32)</option>
 <option value="BZ-BLZ-084">Belize (le) (+501)</option>
 <option value="BJ-BEN-204">Bénin (le) (+229)</option>
 <option value="BM-BMU-060">Bermudes (les) (+1-441)</option>
 <option value="BT-BTN-064">Bhoutan (le) (+975)</option>
 <option value="BO-BOL-068">Bolivie (État plurinational de) (+591)</option>
 <option value="BA-BIH-070">Bosnie-Herzégovine (la) (+387)</option>
 <option value="BW-BWA-072">Botswana (le) (+267)</option>
 <option value="BR-BRA-076">Brésil (le) (+55)</option>
 <option value="BN-BRN-096">Brunéi Darussalam (le) (+673)</option>
 <option value="BG-BGR-100">Bulgarie (la) (+359)</option>
 <option value="BF-BFA-854">Burkina Faso (le) (+226)</option>
 <option value="BI-BDI-108">Burundi (le) (+257)</option>
 <option value="CV-CPV-132">Cabo Verde (+238)</option>
 <option value="KH-KHM-116">Cambodge (le) (+855)</option>
 <option value="CM-CMR-120">Cameroun (le) (+237)</option>
 <option value="CA-CAN-124">Canada (le) (+1)</option>
 <option value="KY-CYM-136">Caïmans (les Îles) (+1-345)</option>
 <option value="CF-CAF-140">République centrafricaine (la) (+236)</option>
 <option value="TD-TCD-148">Tchad (le) (+235)</option>
 <option value="CL-CHL-152">Chili (le) (+56)</option>
 <option value="CN-CHN-156">Chine (la) (+86)</option>
 <option value="CX-CXR-162">Christmas (l'Île) (+53)</option>
 <option value="CC-CCK-166">Cocos (les Îles)/ Keeling (les Îles) (+61)</option>
 <option value="CO-COL-170">Colombie (la) (+57)</option>
 <option value="KM-COM-174">Comores (les) (+269)</option>
 <option value="CD-COD-180">Congo (la République démocratique du) (+243)</option>
 <option value="CG-COG-178">Congo (le) (+242)</option>
 <option value="CK-COK-184">Cook (les Îles) (+682)</option>
 <option value="CR-CRI-188">Costa Rica (le) (+506)</option>
 <option value="HR-HRV-191">Croatie (la) (+385)</option>
 <option value="CU-CUB-192">Cuba (+53)</option>
 <option value="CY-CYP-196">Chypre (+357)</option>
 <option value="CZ-CZE-203">Tchéquie (la) (+420)</option>
 <option value="CI-CIV-384">Côte d'Ivoire (la) (+225)</option>
 <option value="DK-DNK-208">Danemark (le) (+45)</option>
 <option value="DJ-DJI-262">Djibouti (+253)</option>
 <option value="DM-DMA-212">Dominique (la) (+1-767)</option>
 <option value="DO-DOM-214">dominicaine (la République) (+1-829)</option>
 <option value="EC-ECU-218">Équateur (l') (+593)</option>
 <option value="EG-EGY-818">Égypte (l') (+20)</option>
 <option value="SV-SLV-222">El Salvador (+503)</option>
 <option value="GQ-GNQ-226">Guinée équatoriale (la) (+240)</option>
 <option value="ER-ERI-232">Érythrée (l') (+291)</option>
 <option value="EE-EST-233">Estonie (l') (+372)</option>
 <option value="SZ-SWZ-748">Eswatini (l') (+268)</option>
 <option value="ET-ETH-231">Éthiopie (l') (+251)</option>
 <option value="FK-FLK-238">Falkland (les Îles)/Malouines (les Îles) (+500)</option>
 <option value="FO-FRO-234">Féroé (les Îles) (+298)</option>
 <option value="FJ-FJI-242">Fidji (les) (+679)</option>
 <option value="FI-FIN-246">Finlande (la) (+358)</option>
 <option value="FR-FRA-250">France (la) (+33)</option>
 <option value="GF-GUF-254">Guyane française (la ) (+594)</option>
 <option value="PF-PYF-258">Polynésie française (la) (+689)</option>
 <option value="GA-GAB-266">Gabon (le) (+241)</option>
 <option value="GM-GMB-270">Gambie (la) (+220)</option>
 <option value="GE-GEO-268">Géorgie (la) (+995)</option>
 <option value="DE-DEU-276">Allemagne (l') (+49)</option>
 <option value="GH-GHA-288">Ghana (le) (+233)</option>
 <option value="GI-GIB-292">Gibraltar (+350)</option>
 <option value="GR-GRC-300">Grèce (la) (+30)</option>
 <option value="GL-GRL-304">Groenland (le) (+299)</option>
 <option value="GD-GRD-308">Grenade (la) (+1-473)</option>
 <option value="GP-GLP-312">Guadeloupe (la) (+590)</option>
 <option value="GU-GUM-316">Guam (+1-671)</option>
 <option value="GT-GTM-320">Guatemala (le) (+502)</option>
 <option value="GN-GIN-324">Guinée (la) (+224)</option>
 <option value="GW-GNB-624">Guinée-Bissau (la) (+245)</option>
 <option value="GY-GUY-328">Guyana (le) (+592)</option>
 <option value="HT-HTI-332">Haïti (+509)</option>
 <option value="VA-VAT-336">Saint-Siège (le) (+418)</option>
 <option value="HN-HND-340">Honduras (le) (+504)</option>
 <option value="HK-HKG-344">Hong Kong (+852)</option>
 <option value="HU-HUN-348">Hongrie (la) (+36)</option>
 <option value="IS-ISL-352">Islande (l') (+354)</option>
 <option value="IN-IND-356">Inde (l') (+91)</option>
 <option value="ID-IDN-360">Indonésie (l') (+62)</option>
 <option value="IR-IRN-364">Iran (République Islamique d') (+98)</option>
 <option value="IQ-IRQ-368">Iraq (l') (+964)</option>
 <option value="IE-IRL-372">Irlande (l') (+353)</option>
 <option value="IL-ISR-376">Israël (+972)</option>
 <option value="IT-ITA-380">Italie (l') (+39)</option>
 <option value="JM-JAM-388">Jamaïque (la) (+1-876)</option>
 <option value="JP-JPN-392">Japon (le) (+81)</option>
 <option value="JO-JOR-400">Jordanie (la) (+962)</option>
 <option value="KZ-KAZ-398">Kazakhstan (le) (+7)</option>
 <option value="KE-KEN-404">Kenya (le) (+254)</option>
 <option value="KI-KIR-296">Kiribati (+686)</option>
 <option value="KP-PRK-408">Corée (la République populaire démocratique de) (+850)</option>
 <option value="KR-KOR-410">Corée (la République de) (+82)</option>
 <option value="KW-KWT-414">Koweït (le) (+965)</option>
 <option value="KG-KGZ-417">Kirghizistan (le) (+996)</option>
 <option value="LA-LAO-418">Lao (la République démocratique populaire) (+856)</option>
 <option value="LV-LVA-428">Lettonie (la) (+371)</option>
 <option value="LB-LBN-422">Liban (le) (+961)</option>
 <option value="LS-LSO-426">Lesotho (le) (+266)</option>
 <option value="LR-LBR-430">Libéria (le) (+231)</option>
 <option value="LY-LBY-434">Libye (la) (+218)</option>
 <option value="LI-LIE-438">Liechtenstein (le) (+423)</option>
 <option value="LT-LTU-440">Lituanie (la) (+370)</option>
 <option value="LU-LUX-442">Luxembourg (le) (+352)</option>
 <option value="MO-MAC-446">Macao (+853)</option>
 <option value="MG-MDG-450">Madagascar (+261)</option>
 <option value="MW-MWI-454">Malawi (le) (+265)</option>
 <option value="MY-MYS-458">Malaisie (la) (+60)</option>
 <option value="MV-MDV-462">Maldives (les) (+960)</option>
 <option value="ML-MLI-466">Mali (le) (+223)</option>
 <option value="MT-MLT-470">Malte (+356)</option>
 <option value="MH-MHL-584">Marshall (les Îles) (+692)</option>
 <option value="MQ-MTQ-474">Martinique (la) (+596)</option>
 <option value="MR-MRT-478">Mauritanie (la) (+222)</option>
 <option value="MU-MUS-480">Maurice (+230)</option>
 <option value="YT-MYT-175">Mayotte (+269)</option>
 <option value="MX-MEX-484">Mexique (le) (+52)</option>
 <option value="FM-FSM-583">Micronésie (États fédérés de) (+691)</option>
 <option value="MD-MDA-498">Moldova (la République de) (+373)</option>
 <option value="MC-MCO-492">Monaco (+377)</option>
 <option value="MN-MNG-496">Mongolie (la) (+976)</option>
 <option value="MS-MSR-500">Montserrat (+1-664)</option>
 <option value="MA-MAR-504">Maroc (le) (+212)</option>
 <option value="MZ-MOZ-508">Mozambique (le) (+258)</option>
 <option value="MM-MMR-104">Myanmar (le) (+95)</option>
 <option value="NA-NAM-516">Namibie (la) (+264)</option>
 <option value="NR-NRU-520">Nauru (+674)</option>
 <option value="NP-NPL-524">Népal (le) (+977)</option>
 <option value="NL-NLD-528">Pays-Bas (Royaume des) (+31)</option>
 <option value="NC-NCL-540">Nouvelle-Calédonie (la) (+687)</option>
 <option value="NZ-NZL-554">Nouvelle-Zélande (la) (+64)</option>
 <option value="NI-NIC-558">Nicaragua (le) (+505)</option>
 <option value="NE-NER-562">Niger (le) (+227)</option>
 <option value="NG-NGA-566">Nigéria (le) (+234)</option>
 <option value="NU-NIU-570">Niue (+683)</option>
 <option value="NF-NFK-574">Norfolk (l'Île) (+672)</option>
 <option value="MK-MKD-807">Macédoine du Nord (la) (+389)</option>
 <option value="MP-MNP-580">Mariannes du Nord (les Îles) (+1-670)</option>
 <option value="NO-NOR-578">Norvège (la) (+47)</option>
 <option value="OM-OMN-512">Oman (+968)</option>
 <option value="PK-PAK-586">Pakistan (le) (+92)</option>
 <option value="PW-PLW-585">Palaos (les) (+680)</option>
 <option value="PS-PSE-275">Palestine, État de (+970)</option>
 <option value="PA-PAN-591">Panama (le) (+507)</option>
 <option value="PG-PNG-598">Papouasie-Nouvelle-Guinée (la) (+675)</option>
 <option value="PY-PRY-600">Paraguay (le) (+595)</option>
 <option value="PE-PER-604">Pérou (le) (+51)</option>
 <option value="PH-PHL-608">Philippines (les) (+63)</option>
 <option value="PL-POL-616">Pologne (la) (+48)</option>
 <option value="PT-PRT-620">Portugal (le) (+351)</option>
 <option value="PR-PRI-630">Porto Rico (+1-939)</option>
 <option value="QA-QAT-634">Qatar (le) (+974)</option>
 <option value="RO-ROU-642">Roumanie (la) (+40)</option>
 <option value="RU-RUS-643">Russie (la Fédération de) (+7)</option>
 <option value="RW-RWA-646">Rwanda (le) (+250)</option>
 <option value="RE-REU-638">Réunion (La) (+262)</option>
 <option value="SH-SHN-654">Sainte-Hélène, Ascension et Tristan da Cunha (+290)</option>
 <option value="KN-KNA-659">Saint-Kitts-et-Nevis (+1-869)</option>
 <option value="LC-LCA-662">Sainte-Lucie (+1-758)</option>
 <option value="PM-SPM-666">Saint-Pierre-et-Miquelon (+508)</option>
 <option value="VC-VCT-670">Saint-Vincent-et-les Grenadines (+1-784)</option>
 <option value="WS-WSM-882">Samoa (le) (+685)</option>
 <option value="SM-SMR-674">Saint-Marin (+378)</option>
 <option value="ST-STP-678">Sao Tomé-et-Principe (+239)</option>
 <option value="SA-SAU-682">Arabie saoudite (l') (+966)</option>
 <option value="SN-SEN-686">Sénégal (le) (+221)</option>
 <option value="SC-SYC-690">Seychelles (les) (+248)</option>
 <option value="SL-SLE-694">Sierra Leone (la) (+232)</option>
 <option value="SG-SGP-702">Singapour (+65)</option>
 <option value="SK-SVK-703">Slovaquie (la) (+421)</option>
 <option value="SI-SVN-705">Slovénie (la) (+386)</option>
 <option value="SB-SLB-090">Salomon (les Îles) (+677)</option>
 <option value="SO-SOM-706">Somalie (la) (+252)</option>
 <option value="ZA-ZAF-710">Afrique du Sud (l') (+27)</option>
 <option value="ES-ESP-724">Espagne (l') (+34)</option>
 <option value="LK-LKA-144">Sri Lanka (+94)</option>
 <option value="SD-SDN-729">Soudan (le) (+249)</option>
 <option value="SR-SUR-740">Suriname (le) (+597)</option>
 <option value="SE-SWE-752">Suède (la) (+46)</option>
 <option value="CH-CHE-756">Suisse (la) (+41)</option>
 <option value="SY-SYR-760">République arabe syrienne (la) (+963)</option>
 <option value="TW-TWN-158">Taïwan (Province de Chine) (+886)</option>
 <option value="TJ-TJK-762">Tadjikistan (le) (+992)</option>
 <option value="TZ-TZA-834">Tanzanie (la République-Unie de) (+255)</option>
 <option value="TH-THA-764">Thaïlande (la) (+66)</option>
 <option value="TK-TKL-772">Tokelau (les) (+690)</option>
 <option value="TO-TON-776">Tonga (les) (+676)</option>
 <option value="TT-TTO-780">Trinité-et-Tobago (la) (+1-868)</option>
 <option value="TN-TUN-788">Tunisie (la) (+216)</option>
 <option value="TM-TKM-795">Turkménistan (le) (+993)</option>
 <option value="TC-TCA-796">Turks-et-Caïcos (les Îles) (+1-649)</option>
 <option value="TV-TUV-798">Tuvalu (les) (+688)</option>
 <option value="TR-TUR-792">Türkiye (la) (+90)</option>
 <option value="UG-UGA-800">Ouganda (l') (+256)</option>
 <option value="UA-UKR-804">Ukraine (l') (+380)</option>
 <option value="AE-ARE-784">Émirats arabes unis (les) (+971)</option>
 <option value="GB-GBR-826">Royaume-Uni de Grande-Bretagne et d'Irlande du Nord (le) (+44)</option>
 <option value="US-USA-840">États-Unis d'Amérique (les) (+1)</option>
 <option value="UY-URY-858">Uruguay (l') (+598)</option>
 <option value="UZ-UZB-860">Ouzbékistan (l') (+998)</option>
 <option value="VU-VUT-548">Vanuatu (le) (+678)</option>
 <option value="VE-VEN-862">Venezuela (République bolivarienne du) (+58)</option>
 <option value="VN-VNM-704">Viet Nam (le) (+84)</option>
 <option value="VI-VIR-850">Vierges des États-Unis (les Îles) (+1-284)</option>
 <option value="WF-WLF-876">Wallis-et-Futuna (+681)</option>
 <option value="YE-YEM-887">Yémen (le) (+967)</option>
 <option value="ZM-ZMB-894">Zambie (la) (+260)</option>
 <option value="ZW-ZWE-716">Zimbabwe (le) (+263)</option>
 <option value="FR-FRA-COR">Corse (+33)</option>
 </select>
 <p class="describe-input" id="form-sample-phone-prefix-desc">
 Choisir un pays dans la liste
 </p>
 </div>
 </div>
 <div class="s-12 m-6 md-label">
 <label for="form-sample-phone" data-optional="facultatif">N° de téléphone local</label>
 <input type="tel" autocomplete="tel" enterkeyhint="done"
 pattern="0[1-9]{1}[0-9]{8}" placeholder="N° à 10 chiffres" id="form-sample-phone" aria-describedby="form-sample-phone-desc">
 <p class="describe-input" id="form-sample-phone-desc">Exemple : 0123456789 </p>
 </div>
 </fieldset>
 <div class="flex justify-content-space-between s-12 no-fit flex-center">
 <span>
 <button type="reset" class="cta-icon cta-text cta-small no-fit"><svg aria-hidden="true" width="32" height="32">
<use href="#icon-reload"/>
</svg> Rétablir le formulaire
 </button>
 </span>
 <button type="submit" class="cta-link cta-icon no-fit" aria-label="Tester le formulaire d'exemple">
<svg aria-hidden="true" width="32" height="32">
<use href="#icon-round-right"/>
</svg>
 Tester
 </button>
 </div>
 </fieldset>
</form>Nous avons donc trois champs et une liste avec leurs label associés, la description du format attendu qui sera lue par les lecteurs d'écran grâce à l'attribut aria-describedby dont la valeur est l'identifiant du bloc de description en dessous des champs obligatoires et enfin un button de type submit.
Le choix est fait d'afficher directement le format attendu en dessous des champs plutôt qu'à la validation du formulaire qui aura pour effet de faire « sauter » la page en décalant les éléments. Ils seront simplement mis en surbrillance en cas d'erreur.
Dans ce même formulaire, un span invisible avec un identifiant est placé pour permettre de positionner le bloc de gestion des erreurs et le résultat de l'envoi du formulaire.
Affichage des erreurs
C'est là que les choses se corsent. Ou pas.
À la validation, un bloc s'affiche, qui va reprendre la liste des erreurs, en donner le nombre et contenir des boutons (et pas des liens) ayant un contenu textuel clair. Tout ceci sera lu par les lecteurs d'écran. Ce bloc est placé au niveau de notre span invisible, en haut du formulaire.
Et ça donne quoi ? Et bien, il suffit de valider le formulaire d'exemple ci-dessus avec différentes combinaisons
de saisie. Sinon, sur la page de contact,
c'est le même principe.
Bien entendu, lorsqu'un champ est facultatif, mais qu'il est renseigné, il faut également effectuer les contrôles
de validité.
Visuellement, ça devrait ressembler à ça :
Le code en sortie donne ça, avec des options pour utiliser des balises différentes.
Les attributs aria-* pour la gestion du niveau de titre font aussi partie des paramètres
optionnels.
<section role="alert" aria-describedby="js-message-dialog-desc" class="message-dialog error">
 <p id="js-message-dialog-title" role="heading" aria-level="3">
 Le formulaire n'a pas été envoyé car il contient 2 erreurs ;:
 </p>
 <ol id="js-message-dialog-desc">
 <li>
 <button type="button" class="button-item-error">Renseigner le nom ;;</button>
 </li>
 <li>
 <button type="button" class="button-item-error">Le courriel n'est pas valide.</button>
 </li>
 </ol>
</section>Sources pour la gestion des erreurs de formulaires
Fonction de gestion des messages
Normalement, ce code est plutôt compréhensible pour toute personne connaissant Javascript, sans être expert.
Taille : 5,68 kilo-octets (~2,22 kilo-octets compressé)
/*
 * Source Kortic (URL : https://www.kortic.com/formulaires-et-messages-d-erreurs-accessibles.html)
 * licence Creative Commons CC BY-NC-SA 4.0
 * https://creativecommons.org/licenses/by-nc-sa/4.0/deed.fr
 */
 
/*
 Paramètres
 • section : tag de bloc. Par défaut 'div' ;
 • className : class CSS par défaut utilisée dans la feuille de styles ;
 • css : ajout de classes CSS supplémentaires pour la mise en page selon le contexte ;
 • title : ajout d'un titre dans la boite de dialogue ;
 • headingLevel : niveau de titre aria Hn du bloc ou `null` pour ne pas appliquer de niveau de titre ;
 • role : alertdialog par défaut ;
 • point: élément dans la page qui permet de positionner la boite de dialogue. Si null, la boite
 est ajoutée au début du MAIN de la page ou du BODY ;
 • audible : n'affiche pas le message mais sera lu par un lecteur d'écran ;
 • tag : type de tag par défaut de la liste des messages. Si 'ol', la liste est numérotée ;
 • messages: objet qui contient les messages à afficher.
 */
;((w, root, body) => {
 'use strict';
 // Fonction de formatage de chaîne façon sprintf (remplace %a par les arguments)
 w.sprintf = function () {
 let args = Array.from(arguments);
 let str = args[0];
 let i = 1;
 str = str.replace("%%", "<<percent>>");
 while (/%[a-z]/.test(str) && i < args.length) {
 str = str.replace(/%[a-z]/, args[i++]);
 }
 return str.replace("<<percent>>", "%");
 };
 // Supprime tous les éléments d'erreur existants dans le DOM
 w.remove_errors = function (className) {
 document.querySelectorAll(`.${className}`).forEach(el => el.remove());
 };
 // Affiche une boîte de dialogue contenant les erreurs de formulaire
 w.form_errors = function (set) {
 // Paramètres par défaut fusionnés avec ceux fournis
 const settings = Object.assign({
 section: 'div',
 className: 'message-dialog',
 css: null,
 title: null,
 headingLevel: '2',
 role: 'alertdialog',
 point: null,
 audible: false,
 tag: 'ul',
 messages: []
 }, set);
 // Nettoyage des erreurs précédentes
 w.remove_errors(settings.className);
 const num = settings.messages.length;
 if (num === 0) {
 console.warn('Il faut au moins un message dans les paramètres…');
 return;
 }
 // Définition de la ponctuation selon la langue
 const punctuation = {
 in: root.getAttribute('lang') === 'fr' ? ' ;' : ';',
 last: '.'
 };
 // Création du conteneur principal
 const container = document.createElement(settings.section);
 container.classList.add(`${settings.className}`);
 if (settings.css) {
 container.classList.add(...settings.css.split(' '));
 }
 if (settings.audible) {
 container.classList.add('screen-readers');
 }
 // Accessibilité : rôle et focus
 container.setAttribute('role', settings.role);
 container.setAttribute('aria-live', 'assertive');
 container.setAttribute('tabindex', '-1');
 // Création du titre
 const titleEl = document.createElement('p');
 titleEl.id = `js-${settings.className}-title`;
 titleEl.setAttribute('aria-describedby', `js-${settings.className}-desc`);
 titleEl.textContent = settings.title || (
 num === 1
 ? i18n['form_list_error_title']
 : w.sprintf(i18n['form_list_errors_title'], num)
 );
 if (settings.headingLevel !== null) {
 titleEl.setAttribute('role', 'heading');
 titleEl.setAttribute('aria-level', settings.headingLevel);
 }
 container.appendChild(titleEl);
 // Création de la liste des messages
 const listEl = document.createElement(settings.tag);
 listEl.id = `js-${settings.className}-desc`;
 listEl.className = 'ignore-dot';
 settings.messages.forEach((msg, i) => {
 const final = i < num - 1 ? punctuation.in : punctuation.last;
 const li = document.createElement('li');
 // Si le message est un objet [texte, id], on crée un bouton interactif
 if (typeof msg === 'object') {
 const button = document.createElement('button');
 button.type = 'button';
 button.textContent = msg[0] + final;
 // Ajout d’un label pour les lecteurs d’écran
 const span = document.createElement('span');
 span.className = 'screen-readers';
 span.textContent = i18n['form_list_button_aria_label'];
 // Ajout dynamique du span au focus
 button.addEventListener('focus', () => {
 if (!button.contains(span)) {
 button.appendChild(span);
 }
 });
 // Suppression du span au blur
 button.addEventListener('blur', () => {
 if (button.contains(span)) {
 button.removeChild(span);
 }
 });
 // Focus sur l’élément cible au clic
 button.addEventListener('click', () => {
 const target = document.getElementById(msg[1]);
 if (target) target.focus();
 });
 // Marque l’élément cible comme invalide
 const target = document.getElementById(msg[1]);
 if (target) target.setAttribute('aria-invalid', 'true');
 li.appendChild(button);
 } else {
 // Message simple en texte
 li.innerHTML = msg + final;
 }
 listEl.appendChild(li);
 });
 container.appendChild(listEl);
 // Insertion dans le DOM selon le point de référence
 const main = document.querySelector('main');
 if (settings.point) {
 settings.point.insertAdjacentElement('afterend', container);
 } else if (main) {
 main.insertAdjacentElement('afterbegin', container);
 } else {
 body.insertAdjacentElement('afterbegin', container);
 }
 // Focus sur le conteneur pour accessibilité
 container.focus();
 };
})(window, document.documentElement, document.body);
Feuille de styles des messages
Taille : 4,84 kilo-octets (~1,83 kilo-octets compressé)
/*
pictogrammes SVG en variables CSS
--svg-info: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M14.06,9.7c0-.8.65-1.45,1.45-1.45h.97c.8,0,1.45.65,1.45,1.45v.97c0,.8-.65,1.45-1.45,1.45h-.97c-.8,0-1.45-.65-1.45-1.45v-.97ZM19.87,23.75h-7.75v-1.94h1.94v-5.81h-1.94v-1.94h5.81v7.75h1.94v1.94ZM16,.5C7.44.5.5,7.44.5,16s6.94,15.5,15.5,15.5,15.5-6.94,15.5-15.5c0-8.56-6.94-15.5-15.5-15.5,0,0,0,0,0,0ZM16,28.59c-6.96,0-12.59-5.64-12.59-12.59S9.04,3.41,16,3.41s12.59,5.64,12.59,12.59c0,6.96-5.64,12.59-12.59,12.59h0Z"/></svg>');
--svg-notification: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M31.3603,23.5634,20.14936,3.48735a4.69431,4.69431,0,0,0-8.28636,0L.64547,23.5634A4.95709,4.95709,0,0,0,4.78865,31h22.389A4.98211,4.98211,0,0,0,31.3603,23.5634ZM15.9996,26.54621a1.84655,1.84655,0,0,1,0-3.691,1.84835,1.84835,0,0,1,0,3.691Zm1.627-11.93122c-.07905,1.43011-.16468,2.85341-.24372,4.28352-.03952.46309-.03952.88531-.03952,1.34158a1.36579,1.36579,0,0,1-1.34374,1.34159A1.33447,1.33447,0,0,1,14.65586,20.281c-.11856-2.22688-.24371-4.41291-.36228-6.6398-.03952-.58567-.079-1.17814-.12515-1.7638a2.00056,2.00056,0,0,1,1.38325-2.01578,1.77528,1.77528,0,0,1,2.07489,1.04875,2.2658,2.2658,0,0,1,.16467.92616C17.75172,12.76946,17.66609,13.69563,17.62657,14.615Z"/></svg>');
--svg-thumb: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="m28.67,10.8h-8.89l1.34-6.4.04-.45c0-.58-.24-1.1-.62-1.48l-1.49-1.47s-9.99,9.59-10.1,9.8v18.2h15.5c1.17,0,2.17-.7,2.59-1.71l4.26-9.87c.13-.32.2-.66.2-1.02v-2.8c0-1.54-1.27-2.8-2.82-2.8h0Zm-22.54,0H.5v18.2h5.64V10.8h0Z"/></svg>');
 */
.message-dialog {
 background-color: rgb(255,255,255);
 color: black;
 border-radius: var(--radius, 0.1875rem);
 font-family: inherit;
 font-size: inherit;
 margin-block: var(--half-gutter, 1.25rem);
 padding: var(--small-gutter, 1.25rem) var(--half-gutter, 1.25rem) var(--half-gutter, 1.25rem);
 position: relative;
 & > p:first-child {
 position: relative;
 padding-left: var(--gutter);
 &:not([class*=bloc])::before {
 background-color: currentColor;
 -webkit-mask-repeat: no-repeat;
 -webkit-mask-image: var(--svg-info);
 -webkit-mask-position: left center;
 -webkit-mask-size: 1.25rem 1.25rem;
 mask-repeat: no-repeat;
 mask-image: var(--svg-info);
 mask-position: left center;
 mask-size: 1.25rem 1.25rem;
 width: var(--gutter);
 height: 1.5625rem;
 content: "";
 position: absolute;
 left: 0;
 }
 }
 & ul,
 & ol {
 line-height: inherit;
 }
 & li {
 line-height: inherit;
 padding-left: var(--gutter);
 }
 /* noinspection CssInvalidMediaFeature */
 @media (max-width: 63.9375rem) {
 & > p + ul,
 & > p + ol {
 margin-top: var(--small-gutter, 0.625rem);
 }
 }
 @media (min-width: 64rem) {
 & > p + ul,
 & > p + ol {
 margin-top: var(--tiny-gutter, 0.3125rem);
 }
 }
 & button {
 background-color: rgba(0, 0, 0, 0) !important;
 color: inherit;
 outline-color: currentColor !important;
 }
 /* option liste numérotée dans le script (tag: 'ol') */
 & ol {
 counter-reset: count_errors;
 & button::before {
 content: counter(count_errors) '\A0:\A0';
 counter-increment: count_errors;
 font-size: inherit;
 line-height: inherit;
 text-align: right;
 display: inline-block;
 white-space: nowrap;
 }
 }
 /* default : .error */
 &.error {
 color: var(--basic-color-reverse);
 background-color: var(--basic-color);
 & > p:first-child::before {
 -webkit-mask-image: var(--svg-notification);
 mask-image: var(--svg-notification);
 }
 }
 /* variant : .result */
 &.result {
 background-color: var(--lightgreen, rgb(212,249,201));
 color: black;
 & > p:first-child::before {
 -webkit-mask-image: var(--svg-thumb);
 mask-image: var(--svg-thumb);
 }
 }
}
/* mise en forme des messages d'erreurs liés aux champs */
.describe-input {
 padding-left: 0.625rem;
 padding-top: 0.4375rem;
 font-size: 0.875rem;
 display: block;
}
.hidden-describe-input {
 & .describe-input {
 visibility: collapse;
 }
 & .error ~ .describe-input {
 visibility: initial;
 }
}
label.error,
.error ~ .describe-input {
 color: var(--basic-color);
 font-weight: bolder;
}
.error ~ .describe-input::before {
 content: '';
 width: 1.125rem;
 padding-bottom: 1.125rem;
 background-color: var(--error-color);
 display: inline-flex;
 margin-right: 0.3125em;
 vertical-align: text-bottom;
 -webkit-mask-image: var(--svg-info);
 -webkit-mask-size: 95%;
 -webkit-mask-position: left center;
 -webkit-mask-repeat: no-repeat;
 mask-image: var(--svg-info);
 mask-size: 95%;
 mask-position: left center;
 mask-repeat: no-repeat;
}Les variables Cascading Style Sheets sont disponibles dans la feuille de style ici.
JavaScript Object Notation for Linked Data de messages internationalisés
Ce code est placé dans le code source des pages et est placé dans un objet Javascript window.i18n. Ce qui, en soi, est plutôt marrant vu que ce site n'est pas traduit… Un jour, peut-être ! Au moins, c'est prévu. 😊
Extrait nécessaire pour la fonction :
window.i18n = {
 JSON.parse(`{
 "form_list_error_title": "Le formulaire n'a pas \u00e9t\u00e9 envoy\u00e9 car il contient une erreur\u00a0:",
 "form_list_errors_title": "Le formulaire n'a pas \u00e9t\u00e9 envoy\u00e9 car il contient %n erreurs\u00a0:",
 "form_list_button_aria_label": " S\u00e9lectionner pour acc\u00e9der au champ de saisie"
 }`)
};
Licence Creative commons CC BY-NC-SA 4.0
Lien vers la licence : Creative commons CC BY-NC-SA 4.0
Les sources proposées sur Kortic Anthony Ladeuil sont mises à disposition selon les termes suivants :
- Pas d’utilisation commerciale
-
Vous n'êtes pas autorisé à faire un usage commercial de cette Œuvre, tout ou partie du matériel
la composant.
Bien entendu, la probabilité est faible pour que ce point soit respecté. En cas de sursaut d'honnêteté, vous pouvez payer un café, cette page soulagera votre conscience. - Attribution
- Vous devez créditer l'Œuvre, intégrer un lien vers la licence et indiquer si des modifications ont été effectuées à l'Œuvre. Vous devez indiquer ces informations par tous les moyens raisonnables, sans toutefois suggérer que l'Offrant vous soutient ou soutient la façon dont vous avez utilisé son Œuvre.
- Partage dans les mêmes conditions
-
Dans le cas où vous effectuez un remix, que vous transformez, ou créez à partir du matériel
composant
l'Œuvre originale, vous devez diffuser l'Œuvre modifiée dans les mêmes conditions, c'est-à-dire
avec la même licence avec laquelle l'Œuvre originale a été diffusée.
Pas de restrictions complémentaires Vous n'êtes pas autorisé à appliquer des conditions légales ou des mesures techniques qui restreindraient légalement autrui à utiliser l'Œuvre dans les conditions décrites par la licence.
À défaut de tout ça, un petit message et une référence à Kortic restent les bienvenus.
Limitation de responsabilité
Le code est fourni « tel quel », sans garantie d'aucune sorte, expresse ou implicite. En aucun cas l’auteur ou les détenteurs du copyright ne seront responsables de toute réclamation, dommage ou autre responsabilité, que ce soit dans une action contractuelle, délictuelle ou autre, résultant de, en dehors ou en relation avec le logiciel ou l'utilisation ou d'autres transactions.
Vous aimez cette page ?
Dites-le !
Vous aimez cette page ?
Dites-le !