Window splitter accessible : role separator focalisable
Séparateur de fenêtres (window splitter) conforme RGAA : role separator focalisable, aria-valuenow, alternative en un point unique au glissement (critère 13.10) et annulation.
Un séparateur de fenêtres (window splitter) est une poignée déplaçable entre deux panneaux qui permet d'ajuster leur taille relative, en continu ou entre deux positions fixes. Il n'existe pas d'élément natif : le motif repose sur un role="separator" focalisable (tabindex="0") porteur d'une valeur. À ne pas confondre avec un <hr> ou un trait décoratif, qui portent le même rôle mais ne sont ni focalisables ni valués : le tabindex="0" est précisément ce qui transforme le séparateur en widget.
Quand ce motif est le mauvais choix
Demandez-vous d'abord si un panneau redimensionnable est nécessaire : des largeurs fixes, ou une préférence utilisateur persistée, suffisent souvent. Autres cas où un autre choix est correct :
- Replier et déplier un panneau latéral, sans réglage fin : un simple bouton avec
aria-expandedest plus robuste qu'un séparateur valué. - Séparation purement visuelle entre deux blocs : un
<hr>ou une bordure CSS suffit, sanstabindexni valeur, comme le séparateur décoratif d'une barre d'outils. - Régler une valeur qui n'est pas la taille de deux panneaux (volume, prix…) : c'est un curseur, pas un séparateur.
Critères RGAA applicables
- Critère 13.10 : gestes complexes doublés d'un geste simple. Le test 13.10.2 vise précisément ce motif : faire glisser le séparateur est un geste basé sur le suivi d'une trajectoire, et la même fonction doit être disponible par un contact en un point unique (double clic de réinitialisation, boutons replier et rétablir). Une alternative clavier ne satisfait PAS ce critère, qui porte sur l'usage du pointeur : c'est le contresens le plus fréquent.
- Critère 13.11 : annulation des actions au pointeur. Le redimensionnement doit être validé au relâchement du pointeur et pouvoir être abandonné en cours de glissement (Échap qui restaure la taille initiale, par exemple).
- Critère 7.1 : compatibilité des scripts avec les technologies d'assistance. Le
role="separator"doit exposer un nom accessible,aria-valuenow,aria-valueminetaria-valuemax; sans eux le composant n'a ni rôle exploitable ni valeur restituée. - Critère 7.3 : script contrôlable au clavier et par tout dispositif de pointage. Les flèches doivent déplacer le séparateur et Entrée replier ou restaurer le panneau ; le composant doit rester contrôlable au clavier comme au pointeur.
- Critère 10.7 : prise de focus visible. Un trait de 1 px peut difficilement montrer son focus ; prévoir un contour décalé ou un épaississement visible à la prise de focus.
- Critère 3.3 : contraste des composants d'interface. Les couleurs du séparateur dans ses différents états doivent présenter un rapport de contraste d'au moins 3:1 avec la couleur d'arrière-plan contiguë ; un filet gris très clair sur fond blanc est invisible pour un utilisateur malvoyant.
Interaction clavier attendue
| Touche | Action |
|---|---|
| Flèche gauche / Flèche droite | Déplace un séparateur vertical vers la gauche / la droite. |
| Flèche haut / Flèche bas | Déplace un séparateur horizontal vers le haut / le bas. |
| Entrée | Replie le panneau principal, ou le restaure à sa taille précédente. |
| Début / Fin | En option : taille minimale / maximale du panneau principal. |
| F6 | En option : circule entre les panneaux. |
Rôles et attributs ARIA
role="separator" avec tabindex="0", un nom accessible (aria-label ou aria-labelledby) qui désigne le panneau principal, aria-valuenow (taille du panneau principal, en pourcentage), aria-valuemin et aria-valuemax (ses bornes), et aria-controls vers le panneau principal, celui que la valeur décrit. aria-valuetext rend l'annonce compréhensible : « Panneau latéral, 30 % de la largeur » vaut mieux qu'un « 30 » nu. aria-orientation="vertical" pour un séparateur qui partage l'écran verticalement.
<div class="vue-scindee">
<div id="panneau-lateral" style="inline-size: 30%">
<h2>Fichiers</h2>
…
</div>
<!-- tabindex="0" fait du séparateur un widget et non un trait décoratif.
aria-controls désigne le panneau que aria-valuenow décrit. -->
<div
role="separator"
id="separateur"
tabindex="0"
aria-label="Redimensionner le panneau latéral"
aria-controls="panneau-lateral"
aria-orientation="vertical"
aria-valuenow="30" aria-valuemin="15" aria-valuemax="60"
aria-valuetext="Panneau latéral, 30 % de la largeur"
></div>
<div class="vue-scindee__principal">
<h2>Éditeur</h2>
…
</div>
</div>
<!-- Test 13.10.2 : l'alternative en un point unique. Sans elle (ou sans le
double clic ci-dessous), la seule voie au pointeur est un geste de
trajectoire, et le motif échoue. -->
<button type="button" id="separateur-replier">Replier le panneau latéral</button>
<button type="button" id="separateur-reinitialiser">Rétablir la largeur par défaut</button>
/* Rendu 1 px, ciblable sur 24 px : sans cette zone étendue, la poignée est
inattrapable pour une personne au geste imprécis (bonne pratique WCAG 2.2,
ce n'est pas une exigence RGAA). */
#separateur {
position: relative;
inline-size: 1px;
background: #767676; /* au moins 3:1 avec le fond contigu (critère 3.3) */
cursor: col-resize;
}
#separateur::before {
content: "";
position: absolute;
inset-block: 0;
inset-inline: -12px;
cursor: col-resize;
}
/* Un trait fin exige un indicateur de focus délibéré (critère 10.7). */
#separateur:focus-visible {
outline: 2px solid #0056b3;
outline-offset: 2px;
background: #0056b3;
}
const separateur = document.getElementById('separateur');
const panneau = document.getElementById('panneau-lateral');
const MIN = 15, MAX = 60, DEFAUT = 30;
let derniereLargeur = DEFAUT;
function appliquer(pourcent, { replie = false } = {}) {
const valeur = replie ? 0 : Math.min(MAX, Math.max(MIN, Math.round(pourcent)));
panneau.style.inlineSize = `${valeur}%`;
separateur.setAttribute('aria-valuenow', String(valeur));
// « 30 » seul ne veut rien dire ; c'est ce texte qui est réellement annoncé.
separateur.setAttribute('aria-valuetext',
replie ? 'Panneau latéral replié' : `Panneau latéral, ${valeur} % de la largeur`);
return valeur;
}
// Clavier. Attention : ceci satisfait le critère 7.3, PAS le critère 13.10,
// qui porte sur l'usage du pointeur.
separateur.addEventListener('keydown', (evenement) => {
const courant = Number(separateur.getAttribute('aria-valuenow'));
let prochain = null;
if (evenement.key === 'ArrowRight') prochain = courant + 1;
if (evenement.key === 'ArrowLeft') prochain = courant - 1;
if (evenement.key === 'Home') prochain = MIN;
if (evenement.key === 'End') prochain = MAX;
// Entrée replie en mémorisant la largeur, ou restaure.
if (evenement.key === 'Enter') {
evenement.preventDefault();
if (courant === 0) appliquer(derniereLargeur);
else { derniereLargeur = courant; appliquer(0, { replie: true }); }
return;
}
if (prochain === null) return; // Tab passe sans interception
evenement.preventDefault();
appliquer(prochain);
});
// Test 13.10.2 : chaque route ci-dessous redimensionne d'UN contact, sans
// trajectoire. Le double clic de réinitialisation en est une aussi.
document.getElementById('separateur-reinitialiser')
.addEventListener('click', () => appliquer(DEFAUT));
separateur.addEventListener('dblclick', () => appliquer(DEFAUT));
// Le glissement reste possible, comme AMÉLIORATION, jamais comme seule voie.
separateur.addEventListener('pointerdown', (evenement) => {
separateur.setPointerCapture(evenement.pointerId);
const valeurInitiale = Number(separateur.getAttribute('aria-valuenow'));
const bouger = (e) => {
const rect = separateur.parentElement.getBoundingClientRect();
appliquer(((e.clientX - rect.left) / rect.width) * 100);
};
const relacher = () => {
separateur.removeEventListener('pointermove', bouger);
// Validation au relâchement seulement (critère 13.11).
persisterPreference(Number(separateur.getAttribute('aria-valuenow')));
};
// Échap abandonne le glissement et restaure la taille initiale (critère 13.11).
const annuler = (e) => {
if (e.key !== 'Escape') return;
separateur.removeEventListener('pointermove', bouger);
appliquer(valeurInitiale);
};
separateur.addEventListener('pointermove', bouger);
separateur.addEventListener('pointerup', relacher, { once: true });
document.addEventListener('keydown', annuler, { once: true });
});
À ne pas faire :
<!-- Un séparateur utilisable uniquement par glisser-déposer : l'échec type
du test 13.10.2. Ajouter des flèches clavier ne le corrige PAS, car le
critère 13.10 porte sur l'usage du pointeur. -->
<div class="separateur" onmousedown="demarrerGlissement()"></div>
<!-- role="separator" sans tabindex : un trait décoratif, pas un widget.
Inatteignable au clavier, attributs de valeur ignorés. -->
<div role="separator" aria-valuenow="30"></div>
<!-- Un <hr> déguisé en splitter : <hr> porte role="separator" mais n'est pas
un widget ; les attributs de valeur sont ignorés. -->
<hr role="separator" tabindex="0" aria-valuenow="30">
<!-- Sans nom accessible : « séparateur, 30 ». Redimensionner quoi ? -->
<div role="separator" tabindex="0" aria-valuenow="30" aria-controls="panneau-lateral"></div>
// À ne pas faire : persister à chaque pointermove. Une requête par pixel de
// glissement, et plus aucun moyen d'abandonner (critère 13.11).
const bouger = (e) => { appliquer(calcul(e)); persisterPreference(calcul(e)); };
// À ne pas faire : replier sans mémoriser la largeur précédente. Replier puis
// déplier renvoie le panneau à une taille par défaut que l'utilisateur n'a
// jamais choisie.
appliquer(0);
Défauts fréquents et impact utilisateur
- Séparateur utilisable uniquement par glisser-déposer : l'échec type du test 13.10.2 ; une personne qui peut toucher l'écran mais pas suivre une trajectoire (tremblement, pointage à la tête) ne peut pas redimensionner. Ajouter des flèches clavier ne corrige pas ce défaut, qui concerne le pointeur.
role="separator"sanstabindex: ce n'est qu'un filet décoratif ; l'utilisateur clavier ne peut jamais l'atteindre ni le manipuler, et les attributs de valeur sont ignorés.- Pas de nom accessible : le lecteur d'écran annonce « séparateur, 30 » ; redimensionner quoi ?
- Pas d'
aria-valuetext: un « 30 » nu est annoncé ; trente quoi, pour cent de quoi ? aria-controlspointant le mauvais panneau :aria-valuenowdécrit le panneau principal ; s'il pointe l'éditeur alors que la valeur décrit le panneau latéral, l'annonce est inversée.- Zone de saisie de 1 px et repli sans mémoire : un trait de 1 px est impossible à attraper pour une personne au geste imprécis (environ 24 px de zone cliquable est une bonne pratique WCAG 2.2, pas une exigence RGAA) ; et un panneau qui se rouvre à une taille par défaut au lieu de la taille précédente fait perdre son réglage à l'utilisateur.
Ce que les outils automatiques ne détectent pas
Un scanner automatique, le nôtre compris, repère un role="separator" focalisable auquel manque aria-valuenow. Mais le défaut central du motif lui échappe entièrement :
- l'absence d'alternative en un point unique au glissement (test 13.10.2) ne se vérifie qu'en essayant de redimensionner sans glisser ;
- la taille de la zone de saisie autour du trait et la visibilité réelle de l'indicateur de focus sur un élément de 1 px relèvent du test manuel ;
- la mémoire du repli (rouvrir à la taille précédente) et la validation au relâchement (critère 13.11) exigent une manipulation réelle au pointeur.
Vérifier ce composant
Protocole manuel, dans cet ordre :
- Le test du critère 13.10, à lire attentivement : redimensionnez le panneau avec des contacts en un point unique seulement, double clic de réinitialisation ou boutons replier et rétablir. Si la seule voie au pointeur est un glissement, le test 13.10.2 échoue. Les flèches clavier ne sauvent pas ce test.
- Le test du widget : pouvez-vous atteindre le séparateur à Tab ? Sinon, c'est un trait décoratif qui se prend pour un widget :
tabindex="0"manque. - Le test de la zone de saisie : essayez d'attraper la poignée au tactile, ou avec un pointeur imprécis. Une cible de 1 px n'est pas utilisable.
- Au lecteur d'écran : attendez-vous à « Redimensionner le panneau latéral, séparateur, Panneau latéral, 30 % de la largeur ». Un « 30 » nu signale l'absence d'
aria-valuetext. - Le test de la mémoire : repliez, puis dépliez. Le panneau doit revenir à sa taille précédente, pas à une valeur par défaut.
Deux outils gratuits du site pour appuyer la vérification : le simulateur de handicap moteur fait expérimenter ce qu'un glissement de poignée exige avec un pointeur altéré, et pourquoi l'alternative en un point unique n'est pas une option ; le testeur de focus visible valide vos styles :focus, indispensables sur un élément de 1 px (critère 10.7).
Approfondir avec les fiches critères
Vérifier ce composant sur votre site ?
Le scan repère les défauts détectables automatiquement (rôles incohérents, champs sans étiquette, attributs ARIA orphelins) ; le reste se vérifie à la main avec les protocoles de cette fiche.
Lancer un scan gratuit