# « ARIA hidden element must not be focusable » sur un carrousel

> Sur le carrousel d'avis de coopérateurs d'une coopérative agricole, axe-core signale des slides masquées mais encore accessibles au clavier. Diagnostic, correctif et prévention par test automatisé.

- Auteur : WordPress Développement
- Publié le : 2023-07-09
- Mis à jour le : 2023-07-09
- Catégorie : Accessibilité
- URL : https://www.wpmoderne.fr/accessibilite/aria-hidden-focusable-carrousel-cooperative/

## L’essentiel

- Les slides masquées restaient dans l'ordre de tabulation
- La correction synchronise aria-hidden et tabindex
- Un test automatisé empêche la régression au prochain déploiement

« ARIA hidden element must not be focusable ». Ce message d'erreur, remonté par axe-core lors de l'intégration continue d'un site vitrine de coopérative agricole, désignait le carrousel d'avis de coopérateurs affiché sur la page d'accueil. Cinq témoignages défilaient automatiquement, un seul visible à la fois, les quatre autres masqués visuellement par un `overflow: hidden` sur le conteneur. Le problème : ces quatre slides masquées restaient pleinement accessibles au clavier, chacune contenant un lien « Lire l'avis complet » qui recevait le focus normalement.

## Symptôme : un focus qui disparaît visuellement

Le test manuel au clavier révélait le symptôme sans ambiguïté : en appuyant sur `Tab` depuis le premier lien visible du carrousel, le focus suivant sautait vers un lien invisible, quelque part hors de l'écran. Aucune indication visuelle n'accompagnait ce focus, ce qui désoriente autant une personne voyante naviguant au clavier qu'une personne malvoyante utilisant un agrandissement d'écran.

Le code du carrousel avant correction ressemblait à ceci :

```
<div class="carrousel-avis">
  <div class="slide" aria-hidden="false">
    <p>« Coopérative sérieuse, accompagnement au top. »</p>
    <a href="/avis/1">Lire l'avis complet</a>
  </div>
  <div class="slide" aria-hidden="true">
    <p>« Les prix de reprise sont transparents. »</p>
    <a href="/avis/2">Lire l'avis complet</a>
  </div>
  <!-- 3 slides supplémentaires, même structure -->
</div>
```

L'attribut `aria-hidden="true"` était bien posé sur les slides masquées, ce qui aurait dû suffire à les retirer de l'arbre d'accessibilité. Mais la règle WAI-ARIA est stricte sur ce point précis : un élément portant `aria-hidden="true"` ne doit contenir aucun élément focusable, faute de quoi le comportement devient incohérent selon les navigateurs et les technologies d'assistance, d'où l'erreur remontée par axe-core.

## Diagnostic : deux attributs qui doivent évoluer ensemble

> L'essentiel à retenir : Les slides masquées restaient dans l'ordre de tabulation ; La correction synchronise aria-hidden et tabindex ; Un test automatisé empêche la régression au prochain déploiement

Le lien « Lire l'avis complet » restait un élément nativement focusable (une balise `<a href>`), et rien dans le code ne désactivait cette possibilité de focus au moment où la slide passait à l'état masqué. Les deux états — visuellement masqué, focusable au clavier — évoluaient de façon indépendante, alors qu'ils doivent impérativement rester synchronisés.

## Correctif : synchroniser aria-hidden et tabindex par script

La correction ajoute une gestion explicite de l'attribut `tabindex` à chaque changement de slide active, appliquée à tous les éléments focusables d'une slide masquée :

```
function synchroniserSlides() {
  document.querySelectorAll('.slide').forEach((slide) => {
    const masquee = slide.getAttribute('aria-hidden') === 'true';
    slide.querySelectorAll('a, button').forEach((element) => {
      element.setAttribute('tabindex', masquee ? '-1' : '0');
    });
  });
}
```

Cette fonction est appelée à chaque rotation automatique du carrousel, juste après la mise à jour des attributs `aria-hidden` sur chaque slide. Le résultat : un lien masqué visuellement devient également absent de l'ordre de tabulation, exactement comme le prescrit la spécification.

## Prévention : un test automatisé dans la chaîne de déploiement

Pour éviter qu'un futur composant de type carrousel reproduise la même erreur, un test avec Playwright et la bibliothèque `axe-core` a été ajouté à la chaîne de déploiement, exécuté sur la page d'accueil à chaque mise en production :

```
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;

test('carrousel avis sans slide masquee focusable', async ({ page }) => {
  await page.goto('/');
  const resultats = await new AxeBuilder({ page })
    .include('.carrousel-avis')
    .analyze();
  expect(resultats.violations).toEqual([]);
});
```

Ce test échoue désormais automatiquement si un nouveau composant de carrousel reproduit l'erreur, bloquant la mise en production avant qu'elle n'atteigne les visiteurs du site.

> Un attribut `aria-hidden` posé sur un conteneur ne protège jamais, à lui seul, les éléments focusables qu'il contient : la synchronisation avec `tabindex` doit être gérée explicitement, à chaque changement d'état.

## Notre verdict

L'erreur « ARIA hidden element must not be focusable » désigne presque toujours le même défaut : un état visuel masqué qui n'a pas été répercuté sur la possibilité de recevoir le focus. La correction reste simple une fois le diagnostic posé, mais elle mérite un test automatisé durable, car ce type de composant tend à être copié-collé d'un projet à l'autre sans revérification systématique.
