# Construire des onglets accessibles (Tabs) avec l’Interactivity API

> Livrer un composant à onglets sans bibliothèque tierce, avec une gestion du focus et des attributs ARIA conformes, plutôt qu'un simple show/hide en CSS.

- Auteur : Clément Hadrot
- Publié le : 2023-07-06
- Mis à jour le : 2023-07-06
- Catégorie : Blocs Gutenberg
- URL : https://wpmoderne.dev.wordpress-developpement.fr/blocs/onglets-accessibles-tabs-interactivity-api/

## L’essentiel

- Le rôle tablist et les attributs aria-selected structurent la sémantique
- La navigation au clavier par flèches reste attendue par les utilisateurs de lecteurs d'écran
- data-wp-bind synchronise ces attributs sans manipulation manuelle du DOM

Un client du secteur de la formation professionnelle demandait un bloc « Onglets » pour présenter le contenu de ses programmes de formation, une section par onglet (objectifs, prérequis, tarifs, débouchés). La contrainte imposée par son propre cahier des charges accessibilité interdisait toute bibliothèque JavaScript tierce non auditée, ce qui excluait les solutions habituelles basées sur des composants npm généralistes. Le résultat attendu devait pourtant respecter les mêmes exigences qu'un composant à onglets professionnel : gestion du focus au clavier, attributs ARIA corrects, annonce cohérente par un lecteur d'écran.

L'Interactivity API, combinée aux rôles et attributs ARIA du motif « Tabs » documenté par les pratiques d'accessibilité du web, a permis de construire ce composant intégralement en JavaScript natif WordPress, sans dépendance externe, tout en respectant les attentes d'un utilisateur naviguant exclusivement au clavier.

## La structure sémantique attendue

Le motif Tabs repose sur trois rôles ARIA distincts : `tablist` pour le conteneur des boutons d'onglet, `tab` pour chaque bouton individuel, et `tabpanel` pour chaque zone de contenu associée. Chaque onglet référence le panneau qu'il contrôle via `aria-controls`, et chaque panneau référence l'onglet qui le décrit via `aria-labelledby`.

```
<div data-wp-interactive="mon-agence/onglets">
    <div role="tablist">
        <button role="tab" id="tab-objectifs" aria-controls="panel-objectifs"
            data-wp-bind--aria-selected="state.estActif::objectifs"
            data-wp-on--click="actions.selectionner"
            data-wp-context='{"cle": "objectifs"}'>Objectifs</button>
        <button role="tab" id="tab-tarifs" aria-controls="panel-tarifs"
            data-wp-bind--aria-selected="state.estActif::tarifs"
            data-wp-on--click="actions.selectionner"
            data-wp-context='{"cle": "tarifs"}'>Tarifs</button>
    </div>
    <div role="tabpanel" id="panel-objectifs" aria-labelledby="tab-objectifs"
        data-wp-bind--hidden="!state.estActif::objectifs">
        <p>Contenu des objectifs</p>
    </div>
</div>
```

## Le store et la logique de sélection

> L'essentiel à retenir : Le rôle tablist et les attributs aria-selected structurent la sémantique ; La navigation au clavier par flèches reste attendue par les utilisateurs de lecteurs d'écran ; data-wp-bind synchronise ces attributs sans manipulation manuelle du DOM

```
import { store, getContext } from '@wordpress/interactivity';

store( 'mon-agence/onglets', {
    state: {
        ongletActif: 'objectifs',
    },
    actions: {
        selectionner() {
            const { cle } = getContext();
            store( 'mon-agence/onglets' ).state.ongletActif = cle;
        },
    },
} );
```

Chaque bouton porte un contexte local différent via `data-wp-context`, ce qui permet à une seule action `selectionner` partagée de savoir quel onglet a réellement été cliqué, sans dupliquer la logique pour chaque onglet individuellement.

## La navigation au clavier par flèches

Le motif Tabs attendu par les technologies d'assistance impose que les flèches gauche et droite déplacent le focus entre les onglets, sans nécessiter de tabulation répétée. Cette exigence, souvent négligée dans les implémentations improvisées, s'ajoute directement dans l'action de gestion du clavier.

```
actions: {
    naviguerAuClavier( event ) {
        if ( ! [ 'ArrowLeft', 'ArrowRight' ].includes( event.key ) ) return;
        const onglets = Array.from(
            event.target.closest( '[role="tablist"]' ).querySelectorAll( '[role="tab"]' )
        );
        const index = onglets.indexOf( event.target );
        const suivant = event.key === 'ArrowRight'
            ? ( index + 1 ) % onglets.length
            : ( index - 1 + onglets.length ) % onglets.length;
        onglets[ suivant ].focus();
    },
},
```

## Une checklist de conformité avant livraison

- Chaque bouton d'onglet possède-t-il un aria-controls pointant vers un panneau existant ?
- L'attribut aria-selected reflète-t-il correctement l'onglet actif à tout moment ?
- Les flèches gauche et droite déplacent-elles le focus sans activer automatiquement l'onglet visé ?
- Un panneau masqué utilise-t-il l'attribut hidden plutôt qu'un simple display:none en CSS non synchronisé ?

## Tester avec un vrai lecteur d'écran

Aucune checklist ne remplace un test avec un lecteur d'écran réel : sur ce projet, un test avec NVDA a révélé que l'annonce du changement d'onglet restait silencieuse tant que le focus ne se déplaçait pas explicitement vers le panneau ou son contenu après sélection, un détail invisible en lecture de code mais immédiatement perceptible à l'écoute.

> Un composant à onglets qui fonctionne visuellement à la souris ne prouve rien sur son accessibilité réelle. Seul un parcours clavier complet, du premier au dernier onglet, révèle si le motif ARIA a été correctement implémenté.

## En résumé

Ce projet démontre qu'un composant interactif accessible et conforme reste tout à fait accessible sans bibliothèque tierce, à condition de traiter la sémantique ARIA et la navigation clavier comme des exigences de premier ordre, au même titre que l'apparence visuelle. L'Interactivity API fournit les briques nécessaires ; le respect scrupuleux du motif Tabs documenté reste, lui, entièrement à la charge du développeur.
