RGAA Lab

Composants accessibles

Le sélecteur de date

La bonne question n'est pas comment coder un calendrier, mais s'il en faut un. Trois champs jour/mois/année suffisent presque toujours — et coûtent cent fois moins cher. Démo, code, critères RGAA.

6 critères RGAA en jeu

Essayez-le

Format attendu : jour/mois/année, en chiffres — par exemple 14/03/2026.

Tapez 14/03/2026 directement dans le champ : le calendrier n’a jamais eu à s’ouvrir. Ouvrez-le ensuite au clavier — le focus entre sur un jour, les flèches se déplacent de jour en jour et changent de mois toutes seules aux bords, Échap referme et rend le focus au bouton. Choisissez un jour : le focus revient dans le champ, où la valeur vient d’être écrite.

Cette démo existe aussi en page autonome.

L’erreur courante

Le calendrier seul, avec un champ qu’on ne peut pas remplir.

<!-- À ne pas faire : la seule voie d'entrée est le calendrier -->
<input id="naissance" readonly onclick="ouvrirLeCalendrier()" />

C’est le motif le plus répandu des bibliothèques de composants, et il coûte cher à trois personnes différentes.

  • Celui qui connaît sa date. Une date de naissance en 1954, c’est plus de huit cents pressions sur « mois précédent ». Il l’aurait tapée en huit frappes.
  • Celui qui vise mal. Les cases d’un calendrier font trente pixels de côté et se touchent. Un tremblement, et c’est le 13 au lieu du 14 — sans que rien ne le signale.
  • Celui qui ne voit pas la grille. Il doit reconstruire mentalement un tableau de six lignes pour trouver un jour dont il connaissait déjà le quantième.

Le readonly aggrave encore : le champ reste focalisable mais refuse la saisie, si bien qu’on arrive dessus, qu’on tape, et qu’il ne se passe rien.

Trois variantes secondaires, du même esprit :

  • Le format donné seulement dans le message d’erreur. L’utilisateur doit se tromper pour apprendre ce qu’on attendait. Le critère 11.10 demande l’indication du format préalablement à la validation du formulaire.
  • Des <div> cliquables comme jours. Le calendrier s’ouvre au clavier et s’y termine : plus rien n’est atteignable une fois dedans.
  • Le calendrier qui s’ouvre sans que le focus y aille, et qui se ferme sans le rendre. Le focus reste sur le corps de page ; la tabulation suivante repart du haut du document.

Le code, pas à pas

1. Avant tout : faut-il un calendrier ?

Une seule question tranche, et elle ne porte pas sur la technique : l’utilisateur connaît-il déjà la date ?

  • Il la connaît — naissance, date d’une facture, jour d’un rendez-vous déjà pris. Il veut la saisir. Un calendrier ne fait que l’empêcher de taper.
  • Il ne la connaît pas — choisir parmi des créneaux libres, comparer des tarifs selon le jour de la semaine. Il veut voir un mois. Le calendrier gagne son coût.

Dans le premier cas, qui est le plus courant, tout ce qui suit à partir du point 5 est du travail inutile.

2. Trois champs, dans un fieldset

<fieldset>
	<legend>Date de naissance</legend>
	<p id="naissance-format">Par exemple : 14 03 1954</p>

	<label for="naissance-jour">Jour</label>
	<input id="naissance-jour" type="text" inputmode="numeric" maxlength="2"
	       autocomplete="bday-day" aria-describedby="naissance-format" />

	<label for="naissance-mois">Mois</label>
	<input id="naissance-mois" type="text" inputmode="numeric" maxlength="2"
	       autocomplete="bday-month" />

	<label for="naissance-annee">Année</label>
	<input id="naissance-annee" type="text" inputmode="numeric" maxlength="4"
	       autocomplete="bday-year" />
</fieldset>

Le fieldset n’est pas décoratif : le glossaire du référentiel donne « trois champs successifs pour saisir une date (jour/mois/année) » comme premier exemple de champs de même nature. Sans lui, un lecteur d’écran annonce « Jour, champ de saisie » sans dire jour de quoi.

Trois précisions qui font la différence à l’usage. Les étiquettes s’écrivent en toutes lettres — « Jour », pas « JJ » : si vous tenez au format abrégé visible, le nom accessible doit contenir cet intitulé visible, c’est le test 11.2.5. inputmode="numeric" fait apparaître le pavé numérique sur mobile sans les restrictions de type="number". Et autocomplete laisse le navigateur remplir les trois champs d’un coup, ce qui est souvent la différence entre un formulaire long et un formulaire faisable.

Ne déplacez pas le focus automatiquement d’un champ à l’autre quand il est plein : celui qui se corrige se retrouve à taper dans le champ suivant.

3. <input type="date">, ce qu’il donne et ce qu’il coûte

<label for="depart">Date de départ</label>
<input id="depart" type="date" />

Le navigateur fournit alors le clavier, le calendrier, la validation et la traduction — sans une ligne de JavaScript. C’est beaucoup pour un attribut.

Ses limites sont réelles, et il faut les connaître avant de choisir. La présentation du champ ne se style presque pas. Le format affiché suit la langue du système, pas celle de la page : un même formulaire montre jj/mm/aaaa ici et mm/dd/yyyy ailleurs. Et le calendrier déroulant appartient au navigateur — son comportement au clavier varie de l’un à l’autre, vous ne le corrigerez pas.

Il reste le bon choix chaque fois que l’apparence n’est pas contractuelle.

4. Le format, donné avant la saisie

<label for="audit-date">Date de l'audit</label>
<p id="audit-date-format">
	Format attendu : jour/mois/année, en chiffres — par exemple 14/03/2026.
</p>

<input id="audit-date" type="text" aria-describedby="audit-date-format" />

Le test 11.10.5 accepte deux conditions au choix, l’une comme l’autre préalables à la validation : une instruction visible qui identifie nommément le champ concerné, ou une instruction visible située dans l’étiquette ou dans le passage de texte associé au champ. Ici l’indication ne nomme pas le champ ; c’est aria-describedby qui satisfait la seconde, et qui fait restituer le format en même temps que l’étiquette, à l’arrivée dans le champ. Le test n’impose pas la liaison — il impose la visibilité avant la validation.

Quand la saisie échoue, le message répète le format au lieu de constater : « la date s’écrit jour/mois/année, en chiffres — par exemple 14/03/2026 », et non « date invalide ». Le champ porte alors aria-invalid="true", et le message rejoint l’aria-describedby :

<input
	id="audit-date"
	aria-describedby="audit-date-format audit-date-erreur"
	aria-invalid="true"
/>

Acceptez aussi les formes voisines plutôt que de les refuser : 14/3/2026, 14-03-2026, 14 03 2026 disent tous la même chose. Normaliser coûte trois lignes ; refuser coûte un abandon.

5. Si le calendrier est nécessaire : une grille

Un calendrier est un tableau de dates dans lequel on se déplace. C’est exactement ce que décrit role="grid" — et une vraie <table> en fournit déjà la structure.

<p id="calendrier-mois">mars 2026</p>

<table role="grid" aria-labelledby="calendrier-mois">
	<tr>
		<th scope="col"><span aria-hidden="true">L</span><span class="sr-only">Lundi</span></th></tr>
	<tr>
		<td role="gridcell" aria-selected="true">
			<button type="button" tabindex="0" aria-label="samedi 14 mars 2026" aria-current="date">
				14
			</button>
		</td></tr>
</table>

role="grid" change la nature d’interaction du tableau, pas sa structure : les <tr> restent des lignes et les <td> des cellules, il n’y a rien à réécrire. L’aria-labelledby vers le mois affiché donne son nom à la grille.

Le nom accessible de chaque jour porte la date entière — « samedi 14 mars 2026 » — et contient le quantième visible, ce qu’exige le test 7.1.3. Sans lui, un lecteur d’écran annonce « 14, bouton » : quatorze de quel mois ?

aria-selected sur la cellule dit le jour choisi ; aria-current="date" dit le jour d’aujourd’hui. Deux informations différentes, deux attributs différents — et chacune doublée d’un repère visuel qui ne soit pas seulement une couleur.

Un seul jour est tabulable (tabindex="0"), tous les autres portent tabindex="-1" : entrer dans la grille coûte une tabulation, pas trente et une. C’est le même tabindex roulant que dans les onglets, et pour la même raison.

6. Le focus conduit, le mois annoncé

Quatre déplacements de focus, et ils sont tout le composant :

  1. À l’ouverture, le focus entre sur le jour choisi — ou sur aujourd’hui.
  2. Aux flèches, il suit le curseur, y compris quand on sort du mois : la grille se reconstruit autour du nouveau jour.
  3. À Échap, il revient sur le bouton qui a ouvert le calendrier.
  4. Au choix d’un jour, il revient dans le champ de saisie — c’est là que la valeur vient d’être écrite, et le lecteur d’écran la relit avec l’étiquette.

Reste un cas que rien n’annonce : les boutons « mois précédent » et « mois suivant ». Le focus y demeure, leur nom ne change pas, la grille se refait en silence. Il faut donc une région de statut, présente dès le premier rendu et vide :

<!-- Présente au chargement, remplie au changement de mois -->
<p role="status" class="sr-only"></p>

Aux flèches, en revanche, n’annoncez rien : le nom de la cellule atteinte porte déjà « 1 avril 2026 », et l’annonce ferait doublon.

Au clavier

ToucheEffet
TabPasse du champ au bouton du calendrier, puis au reste du formulaire. La grille ouverte ne compte que pour un arrêt.
Jour précédent, jour suivant. Aux bords du mois, la grille change de mois et le focus suit.
Semaine précédente, semaine suivante.
Début / FinPremier ou dernier jour de la semaine où se trouve le focus.
Page préc. / Page suiv.Mois précédent ou suivant, le focus restant sur le même quantième.
Entrée / EspaceChoisit le jour, referme la grille, et pose le focus dans le champ.
ÉchapReferme la grille et rend le focus au bouton qui l’a ouverte.

Ce que le lecteur d’écran restitue

À l’arrivée dans le champ, l’étiquette puis le format — de l’ordre de « Date de l’audit, champ de saisie, format attendu : jour/mois/année, en chiffres, par exemple 14/03/2026 ». Sur le bouton : « Choisir dans le calendrier, bouton, réduit ».

Dans la grille : le nom du mois, la nature du composant, puis la date complète — « mars 2026, grille, samedi 14 mars 2026, sélectionné ». La formulation et la position annoncée varient d’un lecteur d’écran à l’autre ; les informations, non.

Trois diagnostics inverses. Si vous n’entendez que « 14, bouton », le nom accessible de la cellule se limite au quantième. Si vous n’entendez pas le mois en entrant dans la grille, l’aria-labelledby manque. Et si rien ne se dit quand vous activez « mois suivant », c’est qu’il n’y a pas de région de statut — le mois a changé pour l’œil seulement.

Checklist

  • Demandez-vous d’abord si l’utilisateur connaît déjà la date : s’il la connaît, il veut la taper.
  • Trois champs de même nature dans un fieldset avec legend, chacun avec sa propre <label for>.
  • Le format attendu visible avant la saisie, relié par aria-describedby — et répété dans l’erreur.
  • La saisie textuelle reste possible même quand un calendrier existe ; jamais de champ readonly.
  • role="grid" sur une vraie <table>, nommée par le mois affiché, avec un seul jour tabulable.
  • Nom accessible de chaque jour = la date complète ; aria-selected pour le choix, aria-current="date" pour aujourd’hui.
  • Focus conduit aux quatre moments : ouverture, flèches, Échap, choix.
  • Région role="status" présente dès le départ, pour le changement de mois par les boutons.

Les critères RGAA en jeu

Vérifiez votre composant avec l'extension

Pour aller plus loin

Sources