Les API d’accessibilité mobile native en 2026 :
UIAccessibility, AccessibilityNode et le web
iOS et Android exposent chacun un arbre d’accessibilité complet au lecteur d’écran de la plateforme — et ces deux arbres ne s’accordent sur rien au-delà des bases label-et-rôle. Nous avons cartographié chaque primitif que VoiceOver et TalkBack consomment réellement en 2026, la façon dont React Native, Flutter et Kotlin Multiplatform tentent de les réconcilier, et l’endroit où l’accessibilité du WebView mobile chute silencieusement dans le vide.
1. iOS UIAccessibility — labels, traits, hints, valeurs
Tout élément visible sur un écran iOS possède, ou peut posséder, une représentation d’accessibilité. Apple la fournit via le protocole informel UIAccessibility, implémenté par UIView et chaque contrôle système, ainsi que via UIAccessibilityElement, une classe légère que l’on alloue pour les parties de l’interface dessinées mais qui ne sont pas elles-mêmes des vues — des caractères dans un graphique personnalisé, des glyphes dans une scène Core Graphics, des régions à l’intérieur d’un CALayer. VoiceOver, Switch Control, Full Keyboard Access et Voice Control consomment tous le même protocole ; l’apprendre une fois couvre quatre technologies d’assistance.
Le protocole expose quatre primitifs essentiels pour presque tous les écrans. Le label d’accessibilité est le nom court et lisible de l’élément — « Envoyer », « Photo de profil d’Asha », « Retour ». Les traits d’accessibilité sont un masque de bits de drapeaux similaires à des rôles — .button, .header, .image, .selected, .adjustable, .staticText, .updatesFrequently — qui indiquent à VoiceOver comment se comporter autour de l’élément et quels gestes activer. La valeur d’accessibilité est une représentation textuelle de l’état actuel (« Activé », « 75 % », « Jeudi 22 mai »). Le hint d’accessibilité est l’explication plus longue et facultative (« Double-touchez pour ouvrir le visualiseur de photos ») que VoiceOver prononce après un délai si l’utilisateur n’agit pas sur le seul label.
Les quatre primitifs se combinent. Un commutateur s’annonce label + trait + valeur : « Wi-Fi, bouton commutateur, Activé ». Un curseur s’annonce label + trait + valeur + hint : « Volume, ajustable, 60 pour cent, balayez vers le haut ou le bas pour régler ». Une barre de graphique dessinée personnalisée s’exprime comme une chaîne de UIAccessibilityElements, chacun avec un label, une valeur et un cadre dans son conteneur. La chaîne est la surface de l’API — VoiceOver la parcourt linéairement quand l’utilisateur balaye vers la droite, et respecte l’ordre dans lequel les éléments sont publiés via le tableau accessibilityElements du conteneur.
Les modificateurs de vue .accessibilityLabel(), .accessibilityValue(), .accessibilityHint() et .accessibilityAddTraits() en SwiftUI compilent vers les mêmes propriétés UIAccessibility sur la UIView sous-jacente. SwiftUI ajoute aussi .accessibilityElement(children:), qui résout le problème des « caractères dans un graphique » de manière plus déclarative que l’approche UIKit — mais le contrat d’exécution que VoiceOver voit est identique. Apprendre les noms UIKit reste utile, car tous les exemples Apple, toutes les réponses Stack Overflow et tous les audits d’accessibilité les utilisent.
2. Android AccessibilityNodeInfo — rôles, actions, importantForAccessibility
Android emprunte une voie différente. Là où iOS accroche l’accessibilité à un protocole plat sur chaque vue, Android sérialise l’intégralité de l’arbre d’accessibilité sous forme d’un graphe d’objets AccessibilityNodeInfo, chacun étant un instantané d’une vue au moment où une requête TalkBack arrive. Le framework construit les instantanés de manière paresseuse ; une View publie son nœud en surchargeant onInitializeAccessibilityNodeInfo() (ou, dans Compose, en définissant des modificateurs semantics), et la plateforme tisse les relations parent-enfant en un arbre qui reflète la hiérarchie de vues.
Les primitifs diffèrent d’iOS de trois façons significatives. Premièrement, Android expose un rôle via un champ className de type chaîne — android.widget.Button, android.widget.CheckBox, android.widget.EditText. TalkBack lit le nom de classe et décide comment annoncer (« bouton », « case à cocher », « zone de texte »). Compose traduit ses semantics Role.Button, Role.Checkbox, Role.RadioButton dans le même champ. Le rôle est plus précis qu’un masque de bits de traits iOS, mais aussi plus rigide — il n’existe pas de rôle « entièrement personnalisé » sans accepter l’annonce « vue ».
Deuxièmement, Android représente l’interactivité comme un ensemble d’actions attachées au nœud : ACTION_CLICK, ACTION_LONG_CLICK, ACTION_SCROLL_FORWARD, ACTION_SET_TEXT, ACTION_FOCUS, et une longue liste d’actions personnalisées que l’on peut enregistrer avec AccessibilityNodeInfo.AccessibilityAction. TalkBack expose les actions personnalisées via le rotor « actions » — l’utilisateur balaye vers le haut avec un doigt et entend chaque action personnalisée par son nom. iOS dispose du même concept (UIAccessibilityCustomAction), mais sur Android la liste d’actions est la surface ; sur iOS c’est le vocabulaire des gestes.
Troisièmement, Android possède importantForAccessibility, une énumération par vue (auto, yes, no, noHideDescendants) qui contrôle si le nœud apparaît dans l’arbre. noHideDescendants est l’outil le plus puissant de l’accessibilité Android et celui que l’on oublie le plus souvent — il supprime l’intégralité du sous-arbre de la traversée TalkBack, l’équivalent de aria-hidden=“true” sur le web. iOS n’a pas d’équivalent exact ; le plus proche consiste à définir accessibilityElements sur un tableau vide dans le conteneur, ce qui ne supprime que les enfants directs du conteneur, pas tout le sous-arbre.
Android expose ViewCompat.setAccessibilityLiveRegion() avec trois valeurs : none, polite, assertive. Le vocabulaire reflète ARIA — presque. TalkBack respecte les niveaux de politesse de manière fiable. iOS n’a rien de comparable au niveau du protocole : les mises à jour s’annoncent en appelant UIAccessibility.post(notification: .announcement, argument: “Enregistré”), un appel impératif unique qui ne s’attache pas à une vue. Les bridges multiplateformes doivent simuler l’un de ces mécanismes par-dessus l’autre, et l’inadéquation d’impédance se manifeste dans chaque framework examiné à la section 3.
3. Bridges multiplateformes — React Native, Flutter, Kotlin Multiplatform
Chaque framework mobile multiplateforme doit prendre les deux API ci-dessus et présenter une surface unique aux formes du framework. Aucun d’entre eux ne réussit entièrement. Les trois approches dominent le marché en 2026 — React Native, Flutter et Kotlin Multiplatform avec Compose Multiplatform — chacune représentant un compromis légèrement différent entre fuite et abstraction.
accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityState sur Pressable et View se mappent presque 1:1 vers UIAccessibility — mais les noms de rôles sont le vocabulaire React Native, pas celui d’iOS.accessibilityRole=“button” définit className sur android.widget.Button.accessibilityLiveRegion est uniquement Android — sur iOS elle ne fait silencieusement rien, et il faut appeler AccessibilityInfo.announceForAccessibility() manuellement.UIAccessibilityElement sur la vue Flutter, avec des traits mappés depuis les ensembles SemanticsAction et SemanticsFlag.SemanticsFlag.isLiveRegion.Modifier.semantics { } de Compose définit les rôles et les actions une seule fois ; chaque cible traduit le même bloc semantics vers sa propre API a11y native.Le schéma est le même pour tous les trois : un arbre sémantique synthétique aux formes du framework d’un côté, deux arbres d’accessibilité aux formes de la plateforme de l’autre, et un traducteur entre les deux qui gère bien les cas simples et les cas complexes avec une perte de fidélité notable. Les cas simples — un bouton avec un label, une image avec un texte alternatif, un titre — se transmettent sans perte. Les cas complexes — un geste personnalisé à deux doigts, un graphique dont les éléments devraient former un groupe focalisable, une live region qui doit se déclencher sur iOS sans paramètre de politesse lié à une vue — font remonter le vocabulaire de la plateforme sous-jacente dans le code multiplateforme, ou échouent simplement à se traduire.
« Les 80 premiers pour cent de l’accessibilité mobile sont identiques dans tous les frameworks. Les 20 derniers pour cent révèlent quelle API native chaque framework pense secrètement. »
4. La faille WebView — quand l’accessibilité mobile-web échoue silencieusement
iOS et Android affichent tous deux le contenu web via un WebView système — WKWebView sur iOS, android.webkit.WebView (ou Chrome Custom Tabs) sur Android. Dans chaque cas, le WebView est une boîte noire du point de vue de l’application hôte : l’application voit une seule vue, mais le lecteur d’écran voit l’intégralité de l’arbre d’accessibilité DOM à l’intérieur. Le pont entre les deux arbres est l’endroit où une proportion surprenante de l’accessibilité mobile échoue silencieusement.
Le mécanisme est, en apparence, simple. Lorsque le focus d’un lecteur d’écran entre dans un WebView, la plateforme lit l’arbre d’accessibilité du document directement depuis le moteur du navigateur — WebKit sur iOS, Blink sur Android — et le parcourt comme un sous-arbre de l’arbre de l’application hôte. Les rôles, labels et attributs ARIA du web sont traduits en temps réel dans le vocabulaire de la plateforme. Un élément button sans rôle explicite dans le WebView s’annonce comme un bouton sur les deux plateformes ; une région aria-live=“polite” s’annonce correctement sur les deux ; un aria-label sur un lien apparaît comme le label d’accessibilité du lien. Durant les trois premières années de la vie du web mobile, cela fonctionnait simplement.
La falaise apparaît en trois endroits. Premièrement, les gestes personnalisés définis dans l’application hôte — un balayage à deux doigts pour fermer, un magic-tap pour lire et mettre en pause — sont invisibles pour le contenu du WebView ; ils se déclenchent sur la mauvaise cible ou ne se déclenchent pas du tout lorsque le focus est dans le document. Deuxièmement, les UIAccessibilityElements de l’application hôte dessinés par-dessus le WebView (un bouton d’action flottant, une barre d’outils personnalisée) entrent en concurrence avec l’arbre du WebView pour l’ordre de traversée, et l’ordre de lecture qui en résulte est non déterministe selon les versions iOS. Troisièmement — et c’est le mode d’échec le plus important de l’accessibilité mobile-web — le WebView sur iOS ne respecte pas les niveaux de politesse aria-live comme Safari le fait dans un onglet : le plomberie d’annonces de WKWebView abandonne la distinction polite/assertive, de sorte que chaque mise à jour live est traitée comme polite quel que soit le balisage.
<div role="alert" aria-live="assertive">
Connection lost — retrying.
</div>VoiceOver dans un onglet Safari normal interrompt l’énoncé en cours et prononce le message immédiatement. La politesse assertive est respectée de bout en bout via WebKit.
<div role="alert" aria-live="assertive">
Connection lost — retrying.
</div>Même balisage, même moteur de navigateur — mais le bridge d’accessibilité WKWebView vers UIKit rétrograde l’annonce en message polite différé. L’utilisateur l’entend après un délai, parfois après avoir déjà saisi dans le formulaire désormais défaillant.
Pour les annonces dans un WebView, le seul schéma multiplateforme fiable en 2026 est d’exposer un bridge JavaScript vers l’application hôte — un petit gestionnaire postMessage — et de router les annonces assertives hors du DOM, vers l’application hôte, puis à travers UIAccessibility.post(notification: .announcement, …) sur iOS ou announceForAccessibility() sur Android. L’aria-live web ne survit que pour les messages véritablement polis où un délai de quelques secondes est acceptable.
5. La table de correspondance — qui correspond à quoi
Nous avons cartographié 28 primitifs que VoiceOver et TalkBack consomment réellement en pratique — l’union de la surface du protocole iOS UIAccessibility, de la surface Android AccessibilityNodeInfo et des props multiplateformes les plus utilisées de React Native et Flutter. La table ci-dessous ne capture que les lignes contestées : les primitifs où le mapping est incomplet, asymétrique ou surprenant. Les lignes où le mapping est propre (label, rôle bouton, rôle image, titre) ont été omises pour des raisons de longueur.
| Capacité | iOS UIAccessibility | Android AccessibilityNodeInfo | React Native 0.76 | Flutter 3.27 |
|---|---|---|---|---|
| Texte hint (explication longue) | accessibilityHint | tooltipText (API 28+) | accessibilityHint (iOS uniquement) | SemanticsProperties.hint |
| Politesse live region | N/A — post impératif uniquement | setAccessibilityLiveRegion() | accessibilityLiveRegion (Android uniquement) | SemanticsFlag.isLiveRegion |
| Masquer le sous-arbre de l’a11y | accessibilityElementsHidden (enfants uniquement) | importantForAccessibility=“noHideDescendants” | accessibilityElementsHidden / importantForAccessibility | Widget ExcludeSemantics |
| Action personnalisée (rotor / menu) | UIAccessibilityCustomAction | AccessibilityNodeInfo.AccessibilityAction | accessibilityActions + onAccessibilityAction | SemanticsAction avec label personnalisé |
| Semantics ajustable / curseur | UIAccessibilityTraitAdjustable + accessibilityIncrement | RangeInfo + ACTION_SCROLL_FORWARD | accessibilityRole=“adjustable” + gestionnaires | Slider expose SemanticsAction.increase |
| Niveau de titre | UIAccessibilityTraitHeader (sans niveau) | setHeading(true) (sans niveau) | accessibilityRole=“header” (sans niveau) | SemanticsProperties.headingLevel (1–6) |
| État sélectionné / basculé | UIAccessibilityTraitSelected | setSelected(true) + setCheckable() | accessibilityState={selected, checked} | SemanticsFlag.isSelected |
| Semantics groupe / conteneur | shouldGroupAccessibilityChildren | setScreenReaderFocusable(true) | accessible={true} sur le parent | Widget MergeSemantics |
| Annoncer un message ponctuel | UIAccessibility.post(.announcement, …) | view.announceForAccessibility() | AccessibilityInfo.announceForAccessibility() | SemanticsService.announce() |
Trois schémas ressortent de la table. Premièrement, l’asymétrie autour des live regions est la principale source de divergence multiplateforme — Android dispose d’un paramètre de politesse par vue, iOS n’a qu’un post impératif global, et chaque framework ci-dessus est contraint de mentir sur la différence. Deuxièmement, les niveaux de titres sont le seul endroit où Flutter améliore réellement les deux plateformes natives ; les primitifs iOS et Android ne savent que « c’est un titre », pas « c’est un H3 sous un H2 ». Troisièmement, le primitif « masquer de l’accessibilité » est plus flexible sur Android que sur iOS — noHideDescendants masque tout un sous-arbre en un seul geste, tandis qu’iOS nécessite de masquer les enfants de chaque conteneur individuellement.
6. Le guide pratique mobile native
Apprendre le vocabulaire natif avant le vocabulaire du framework
Chaque bridge multiplateforme — React Native, Flutter, Compose Multiplatform — a sa propre nomenclature pour les props d’accessibilité, et chacun de ces noms est un léger mensonge sur ce que la plateforme sous-jacente fait réellement. Quand un lecteur d’écran n’annonce pas correctement, le bug se trouve presque toujours dans l’API native vers laquelle le framework a traduit, et non dans la prop du framework que l’on a définie. Il faut lire la documentation UIAccessibility et la documentation AccessibilityNodeInfo au moins une fois ; la documentation du framework n’a de sens qu’ensuite.
Tester les annonces live sur iOS spécifiquement
L’asymétrie de live region de la section 2 signifie que tout code qui suppose que aria-live=“assertive” ou accessibilityLiveRegion=“assertive” fonctionne va se dégrader silencieusement sur iOS. Il faut construire un petit banc de test qui déclenche une annonce polie et une annonce assertive sur les deux plateformes, avec VoiceOver et TalkBack sur de vrais appareils, avant de livrer toute fonctionnalité dont l’expérience utilisateur dépend de ce que l’utilisateur entende un changement d’état.
Sortir des WebViews via un bridge pour tout ce qui est assertif
La rétrogradation par WKWebView des annonces assertives n’est pas un bug qu’Apple corrigera prochainement — c’est la même chose dans chaque version iOS depuis la version 14. Si une application hybride est livrée où l’utilisateur peut rencontrer une erreur fatale dans un WebView, l’annonce doit être routée via un bridge JS vers l’hôte et laisser l’hôte déclencher l’annonce plateforme. Le web seul ne suffit pas.
Utiliser la sémantique « merge » ou « group » du framework, pas enfant par enfant
iOS (shouldGroupAccessibilityChildren), Android (setScreenReaderFocusable) et Flutter (MergeSemantics) fournissent tous un moyen de regrouper un cluster visuel — une icône plus un label plus une valeur — en un seul élément d’accessibilité. Il faut l’utiliser. Le comportement par défaut « chaque feuille est un élément focalisable » transforme une puce de navigation à six éléments en six balayages VoiceOver.
Auditer avec Accessibility Inspector et les paramètres développeur TalkBack
Les deux plateformes fournissent un inspecteur officiel gratuit pour l’arbre d’accessibilité en direct — Accessibility Inspector sur macOS (couplé au simulateur ou à l’appareil iOS connecté), et la superposition « Afficher le focus d’accessibilité » plus « Paramètres développeur » sur Android. Il faut les utiliser pour lire l’arbre de sa propre application comme le lecteur d’écran le voit ; il ne faut pas supposer que le journal de débogage du framework montre la même chose que ce que la plateforme montre à TalkBack.
Conclusion : le framework est en aval de la plateforme
Il est tentant de croire — et la documentation du framework encourage cette croyance — qu’une API d’accessibilité multiplateforme est une abstraction unique et unifiée au-dessus de deux API natives équivalentes. La table de correspondance de la section 5 réfute cette unification. Les deux API natives ont été conçues indépendamment, par deux équipes différentes, autour de deux modèles mentaux différents de la façon dont le lecteur d’écran devrait parcourir un document ; les différences sont réelles, elles filtrent à travers chaque framework, et les fuites apparaissent dans les parties de l’expérience utilisateur qui comptent le plus — les mises à jour live, les gestes personnalisés, les sous-arbres masqués, les hiérarchies de titres.
La bonne nouvelle, après ce paragraphe : les bases se mappent. Un bouton avec un label, une image avec un texte alternatif, un titre en haut d’une section — ces éléments passent à travers chaque framework et s’annoncent correctement sur les deux plateformes. Si l’on ne livre que ces primitifs, il n’est pas nécessaire de penser à UIAccessibility ou AccessibilityNodeInfo ; les valeurs par défaut du framework sont honnêtes. Les problèmes commencent quand l’interface utilisateur commence à faire quelque chose d’intéressant, ce qui est aussi le moment où l’accessibilité commence à compter le plus.
Le guide pratique de la section 6 est la version la plus courte de l’argument qui donne à davantage d’utilisateurs handicapés une expérience fonctionnelle : penser d’abord en primitifs natifs, tester sur de vrais appareils sur les deux plateformes, sortir des WebViews quand on le souhaite vraiment, regrouper délibérément les nœuds feuilles, et utiliser les inspecteurs officiels. Le framework choisi aide pour les 80 premiers pour cent et s’efface pour les 20 derniers. Ces 20 derniers pour cent sont là où vit l’utilisateur de lecteur d’écran.
« VoiceOver et TalkBack lisent deux documents différents à partir du même code source. Que l’utilisateur remarque la différence est une mesure de la qualité de la compréhension de la plateforme sous-jacente au framework. »