Le glisser-déposer
Aucune touche du clavier n'émet un dragstart : un glisser-déposer sans commande équivalente rend la fonctionnalité inatteignable. Deux boutons par ligne suffisent presque toujours. Démo, code, critères RGAA.
3 critères RGAA en jeu
Essayez-le
- 1 Accueil
- 2 Résultats de recherche
- 3 Formulaire de contact
- 4 Article de blog
- 5 Mentions légales
Après le dernier déplacement, le focus était sur : rien pour l'instant
Saisissez une ligne à la souris et déposez-la ailleurs : le geste fonctionne.
Puis lâchez la souris et refaites le même travail à la tabulation — les boutons Monter et Descendre déplacent la même ligne, d’un rang à la fois.
Montez la dernière ligne jusqu’en tête, un rang à la fois : arrivée en première
position, son bouton Monter devient indisponible, et le focus passe à Descendre de la même ligne plutôt que de disparaître. Le relevé sous la
liste lit document.activeElement après chaque déplacement, au clavier comme
au pointeur, et affiche où le focus est resté.
Cette démo existe aussi en page autonome.
L’erreur courante
Un tri par glisser, et rien d’autre.
<!-- À ne pas faire : le geste est la seule façon de réordonner -->
<ul id="echantillon">
<li draggable="true">Accueil</li>
<li draggable="true">Résultats de recherche</li>
<li draggable="true">Formulaire de contact</li>
</ul> liste.addEventListener('dragstart', prendre);
liste.addEventListener('dragover', (e) => e.preventDefault());
liste.addEventListener('drop', deposer); C’est propre, c’est court, et c’est inutilisable pour une partie des visiteurs.
draggable ne rend pas l’élément focalisable, et aucune touche du clavier
n’émet dragstart : il n’existe pas de séquence, même obscure, qui déclenche
un glisser. La fonctionnalité n’est donc pas « difficile » au clavier, elle est
absente. Ce n’est pas non plus un problème de clavier seulement : l’API de
glisser-déposer HTML n’est pas déclenchée par le tactile sur mobile, et un
geste de précision reste hors de portée pour qui tremble ou pilote au trackball.
Trois variantes, aussi répandues :
- Une alternative qui n’en est pas une. Un bouton « Enregistrer l’ordre » sous la liste n’aide personne : il valide un ordre qu’on n’a pas pu établir. L’alternative doit permettre la même action, pas la suite de l’action.
role="application"posé sur la liste pour capter les flèches. Le lecteur d’écran quitte alors son mode de lecture, et l’utilisateur perd la navigation par éléments dans toute la zone — on paie très cher deux touches.- Un mode « prise au clavier » silencieux. Espace prend, les flèches déplacent, Espace dépose : le motif est bon, mais sans instructions ni annonce il ne se devine pas, et la ligne bouge sous un focus dont personne n’est informé.
Le code, pas à pas
1. Nommer l’opération avant de coder le geste
Un glisser-déposer accessible est presque toujours une réorganisation de liste : l’ordre des colonnes d’un tableau de bord, des pages d’un échantillon, des étapes d’un parcours. L’opération réelle est « déplacer cet élément à cette position ».
Écrivez cette fonction d’abord :
function deplacer(depart, arrivee) {
const [element] = ordre.splice(depart, 1);
ordre.splice(arrivee, 0, element);
} Le geste et les boutons deviennent alors deux façons de l’appeler. Quand on part du geste, l’alternative arrive après coup et ne recouvre jamais tout ce qu’il permettait ; quand on part de l’opération, la question ne se pose plus.
2. Deux boutons par ligne, qui disent laquelle ils déplacent
<li>
<span class="rang">1</span>
<span>Accueil</span>
<button type="button" disabled>Monter<span class="sr-only"> Accueil</span></button>
<button type="button">Descendre<span class="sr-only"> Accueil</span></button>
</li> Le complément masqué n’est pas un ornement. Un lecteur d’écran sait lister les boutons d’une page : sur une liste de cinq lignes, cette liste afficherait dix fois « Monter » et « Descendre » sans rien pour les distinguer. Avec le complément, elle affiche « Monter Accueil », « Descendre Accueil » — et chaque bouton redevient identifiable hors de son contexte.
L’intitulé visible reste « Monter », et le nom accessible le contient : c’est ce que demande le critère 7.1, et c’est aussi ce qui permet de dire « Monter » à la commande vocale et d’être compris.
3. Les extrémités : désactiver, puis rattraper le focus
Le bouton Monter de la première ligne n’a rien à faire. Il faut le dire, et le plus court est de le désactiver :
boutonMonter.disabled = rang === 0; Le retirer du DOM change le nombre de boutons d’une ligne à l’autre et déplace
tout ce qui suit ; le laisser actif et sans effet fait croire à une panne. disabled dit la vérité : la commande existe, elle n’est pas disponible ici.
Le référentiel n’impose pas ce choix : le critère 7.3 exige seulement une
commande équivalente et un focus jamais supprimé. aria-disabled="true" sur un
bouton laissé focalisable satisfait la même exigence et évite le rattrapage de
focus qui suit ; nous préférons disabled parce qu’il porte l’état sans un
attribut de plus.
Mais un élément désactivé ne garde pas le focus. Si l’utilisateur appuie sur Monter jusqu’à ce que sa ligne atteigne la tête, le bouton qu’il vient
d’actionner devient indisponible sous ses doigts, et le focus retombe sur body — il faut alors retabuler depuis le haut de la page pour retrouver sa
place. Le critère 7.3 l’interdit explicitement : un script ne doit pas
supprimer le focus d’un élément qui le reçoit.
// Après le déplacement et le nouveau rendu.
const sens = versLeHaut ? 'monter' : 'descendre';
const actionne = ligne.querySelector(`.${sens}`);
if (actionne.disabled) ligne.querySelector(sens === 'monter' ? '.descendre' : '.monter').focus();
else actionne.focus(); La règle tient en une phrase : le focus reste sur l’élément déplacé. S’il ne peut pas rester sur le même bouton, il passe à l’autre bouton de la même ligne — jamais ailleurs, et jamais nulle part.
4. Annoncer le déplacement, et la position atteinte
<!-- Présente dès le premier rendu, vide. -->
<p role="status"></p> statut.textContent = `${element.nom} déplacé en position ${arrivee + 1} sur ${ordre.length}.`; Deux informations, et il faut les deux. L’action effectuée, parce que rien d’autre ne confirme que l’appui a fait quelque chose. La position atteinte, parce que c’est l’information que l’œil obtient gratuitement en voyant la ligne glisser d’un cran : sans elle, il faut relire toute la liste pour savoir où l’on en est.
role="status" et non role="alert" : c’est le résultat d’une action réussie,
il s’annonce quand le lecteur d’écran a fini sa phrase en cours, sans
l’interrompre. Et la région doit exister avant l’annonce : créée au moment
où l’on écrit dedans, elle n’est pas restituée.
5. Quand la liste est longue : le champ de position, le menu « déplacer vers »
Deux boutons suffisent tant que la liste est courte. Sur vingt éléments, remonter le dernier coûte dix-neuf pressions : la commande existe, mais elle n’est plus praticable. Deux réponses, selon le contexte :
- Un champ de position par ligne —
<input type="number">étiqueté « Position », validé au changement. La ligne va directement où on lui dit. - Un menu « Déplacer vers… » — un bouton par ligne ouvrant la liste des positions ou des groupes cibles. Utile quand les destinations sont des conteneurs nommés plutôt que des rangs.
Le référentiel exige qu’une commande accessible au clavier et à tout dispositif de pointage permette la même action ; il ne dit pas laquelle. Le choix entre ces trois formes est donc le vôtre — c’est une décision d’ergonomie, pas une obligation.
6. Le geste, en second — et faut-il ce composant ?
Gardez le glisser si vos utilisateurs l’attendent : c’est un raccourci confortable pour qui peut le faire. Posez-le par-dessus les boutons, jamais à leur place, et préférez les événements de pointeur à l’API de glisser-déposer HTML, qui ignore le tactile.
Puis posez la question d’avant : la liste a-t-elle vraiment besoin d’être ordonnée à la main ? Un tri par colonne, une date, un champ « ordre » numéroté règlent souvent le besoin réel sans aucun geste ni aucun script de déplacement — et se lisent, s’impriment et se relisent mieux.
Au clavier
| Touche | Effet |
|---|---|
| Tab | Va au bouton suivant : Monter puis Descendre de chaque ligne. |
| Entrée / Espace | Déplace la ligne d’un rang. Le focus reste sur le bouton actionné, ou passe à l’autre bouton de la même ligne si celui-ci vient d’être désactivé. |
| Aucune | Rien ne déclenche un glisser au clavier : dragstart n’est émis que par un pointeur. C’est toute la raison d’être des boutons. |
Il n’existe pas de convention établie pour réordonner une liste au clavier — l’ARIA APG ne publie aucun motif de glisser-déposer. Deux boutons explicites valent donc mieux qu’une combinaison de touches inventée, que rien n’annoncera et que personne ne cherchera.
Ce que le lecteur d’écran restitue
Sur un bouton : le nom complet et sa nature — « Monter Accueil, bouton ». En tête de liste, l’indisponibilité s’y ajoute : « Monter Accueil, bouton, non disponible ». Après l’appui, le message de statut suit tout seul : « Accueil déplacé en position 2 sur 5 ». La formulation varie d’un lecteur à l’autre, les trois informations non.
Si vous n’entendez que « Monter, bouton », le complément masqué manque, et la liste des boutons de la page est illisible. Si le déplacement ne s’annonce pas, la région de statut a été créée au moment de l’annonce au lieu d’exister vide depuis le début. Et si, après un appui, le lecteur d’écran repart du haut de la page, c’est que le bouton actionné a été désactivé ou détruit sans que le focus soit rendu.
Checklist
- Écrire l’opération « déplacer vers » avant d’écrire le geste.
- Une commande atteignable au clavier pour chaque déplacement possible au pointeur.
- Le nom accessible de chaque bouton nomme la ligne qu’il déplace.
- Boutons d’extrémité rendus indisponibles —
disabledici — jamais retirés ni inertes en silence. - Le focus reste sur l’élément déplacé, y compris quand son bouton se désactive.
- Un
role="status"présent dès le premier rendu, qui dit l’action et la position. - Au-delà d’une dizaine de lignes, prévoir un champ de position ou un menu « déplacer vers ».
Les critères RGAA en jeu
- Critère 7.3 — Contrôle des scripts au clavier
Le glisser ne s'atteint qu'au pointeur : aucune touche n'émet dragstart. Sans commande équivalente dans la page, réorganiser la liste est simplement impossible au clavier.
- Critère 7.1 — Compatibilité des scripts
Ce sont les boutons de déplacement qui portent la fonctionnalité : leur nom accessible doit dire quelle ligne ils déplacent, faute de quoi la page n'offre que dix boutons Monter indiscernables.
- Critère 7.5 — Messages de statut
Le déplacement se voit, mais ne s'entend pas : sans message de statut, rien ne dit l'action effectuée ni la position atteinte, et il faut relire toute la liste pour le savoir.
Vérifiez votre composant avec l'extension
Pour aller plus loin
- Critère 7.3 — Contrôle des scripts au clavier : l’exigence d’équivalence, et l’interdiction de supprimer le focus.
- Critère 7.5 — Messages de statut : ce qui distingue
role="status"derole="alert". - « Accessible et activable par le clavier et tout dispositif de pointage » au glossaire : la définition qui fixe ce que « contrôlable » veut dire.
- Les listes : le balisage du contenu qu’on réordonne ici.
Sources
- WCAG — Understanding 2.5.7 Dragging Movements — l’exigence WCAG 2.2 qui vise exactement ce composant, absente du RGAA 4.1
- MDN — HTML Drag and Drop API — ce que l’API fait, et ce qu’elle ne fait pas
- WCAG — Understanding 2.1.1 Keyboard — l’exigence de fonctionnement au clavier
- MDN — ARIA
statusrole — la région qui porte l’annonce du déplacement