API di accessibilità mobile native nel 2026:
UIAccessibility, AccessibilityNode e il web
iOS e Android espongono entrambi un albero di accessibilità completo al lettore di schermo della piattaforma — e i due alberi non concordano su nulla al di là delle basi di etichetta e ruolo. Abbiamo mappato ogni primitiva che VoiceOver e TalkBack consumano concretamente nel 2026, il modo in cui React Native, Flutter e Kotlin Multiplatform cercano di colmare le differenze, e il punto in cui l’accessibilità di WebView su mobile precipita silenziosamente nel vuoto.
1. iOS UIAccessibility — etichette, trait, hint, valori
Ogni elemento visibile su uno schermo iOS ha, o può avere, una rappresentazione di accessibilità. Apple fornisce tale rappresentazione attraverso il protocollo informale UIAccessibility, implementato da UIView e da ogni controllo di sistema, e tramite UIAccessibilityElement, una classe leggera che si alloca per le parti dell’interfaccia disegnate ma non costituite da view — caratteri in un grafico personalizzato, glifi in una scena Core Graphics, regioni all’interno di un CALayer. VoiceOver, Switch Control, Full Keyboard Access e Voice Control consumano tutti lo stesso protocollo; impararlo una sola volta vale quattro tecnologie assistive.
Il protocollo espone quattro primitive rilevanti per quasi ogni schermata. L’etichetta di accessibilità è il nome breve e leggibile dell’elemento — «Invia», «Foto profilo di Asha», «Indietro». I trait di accessibilità sono una bitmask di flag analoghi a ruoli — .button, .header, .image, .selected, .adjustable, .staticText, .updatesFrequently — che indicano a VoiceOver come comportarsi in prossimità dell’elemento e quali gesture abilitare. Il valore di accessibilità è una rappresentazione testuale dello stato corrente («On», «75%», «giovedì 22 maggio»). L’hint di accessibilità è la spiegazione più lunga e opzionale («Tocca due volte per aprire il visualizzatore di foto») che VoiceOver pronuncia dopo un ritardo se l’utente non agisce sulla sola etichetta.
Le quattro primitive si compongono. Uno switch viene letto come etichetta + trait + valore: «Wi-Fi, switch button, On». Uno slider viene letto come etichetta + trait + valore + hint: «Volume, adjustable, 60 percent, swipe up or down to adjust». Una barra di un grafico personalizzato viene letta come una catena di UIAccessibilityElement, ognuno con un’etichetta, un valore e un frame all’interno del proprio container. La catena è la superficie API — VoiceOver la percorre linearmente quando l’utente scorre verso destra, rispettando l’ordine con cui si pubblicano gli elementi tramite l’array accessibilityElements del container.
I modificatori di view .accessibilityLabel(), .accessibilityValue(), .accessibilityHint() e .accessibilityAddTraits() in SwiftUI vengono compilati nelle stesse proprietà UIAccessibility sulla UIView sottostante. SwiftUI aggiunge anche .accessibilityElement(children:), che risolve il problema dei «caratteri in un grafico» in modo più dichiarativo rispetto all’approccio UIKit — ma il contratto a runtime che VoiceOver vede è identico. Vale comunque la pena imparare i nomi UIKit, perché ogni esempio Apple, ogni risposta su Stack Overflow e ogni audit di accessibilità li utilizza.
2. Android AccessibilityNodeInfo — ruoli, azioni, importantForAccessibility
Android adotta un approccio diverso. Mentre iOS appende l’accessibilità a un protocollo piatto su ogni view, Android serializza l’intero albero di accessibilità come un grafo di oggetti AccessibilityNodeInfo, ognuno uno snapshot di una view nel momento in cui arriva una query di TalkBack. Il framework costruisce gli snapshot pigramente; una View pubblica il proprio nodo sovrascrivendo onInitializeAccessibilityNodeInfo() (o, in Compose, impostando modificatori di semantica), e la piattaforma assembla le relazioni genitore-figlio in un albero che rispecchia la gerarchia delle view.
Le primitive differiscono da iOS in tre modi significativi. In primo luogo, Android espone un ruolo tramite un campo className di tipo stringa — android.widget.Button, android.widget.CheckBox, android.widget.EditText. TalkBack legge il nome della classe e decide come annunciarlo («button», «checkbox», «edit box»). Compose traduce i propri semantics Role.Button, Role.Checkbox, Role.RadioButton nello stesso campo. Il ruolo è più granulare di una bitmask di trait iOS, ma anche più rigido — non esiste un ruolo «completamente personalizzato» a meno di non accettare l’annuncio come «view».
In secondo luogo, Android rappresenta l’interattività come un insieme di azioni collegate al nodo: ACTION_CLICK, ACTION_LONG_CLICK, ACTION_SCROLL_FORWARD, ACTION_SET_TEXT, ACTION_FOCUS, e un lungo elenco di azioni personalizzate registrabili con AccessibilityNodeInfo.AccessibilityAction. TalkBack espone le azioni personalizzate tramite il rotore «azioni» — l’utente scorre verso l’alto con un dito e sente ogni azione personalizzata per nome. iOS ha lo stesso concetto (UIAccessibilityCustomAction), ma su Android l’elenco di azioni è la superficie; su iOS lo è il vocabolario dei gesti.
In terzo luogo, Android dispone di importantForAccessibility, un enum per view (auto, yes, no, noHideDescendants) che controlla se il nodo appare nell’albero. noHideDescendants è lo strumento più potente nell’accessibilità Android e quello più spesso dimenticato — rimuove l’intero sottoalbero dall’attraversamento di TalkBack, equivalente ad aria-hidden=“true” sul web. iOS non ha un analogo esatto; il più vicino è impostare accessibilityElements su un array vuoto nel container, che rimuove solo i figli diretti del container, non l’intero sottoalbero.
Android espone ViewCompat.setAccessibilityLiveRegion() con tre valori: none, polite, assertive. Il vocabolario rispecchia ARIA — quasi. TalkBack rispetta i livelli di cortesia in modo affidabile. iOS non ha nulla di paragonabile a livello di protocollo: si annunciano gli aggiornamenti chiamando UIAccessibility.post(notification: .announcement, argument: “Salvato”), un’operazione imperativa una-tantum che non si collega a una view. I bridge multipiattaforma devono simulare uno dei due approcci sull’altro, e lo scarto di impedenza emerge in ogni framework analizzato nella sezione 3.
3. Bridge multipiattaforma — React Native, Flutter, Kotlin Multiplatform
Ogni framework mobile multipiattaforma deve prendere le due API sopra descritte e presentare un’unica superficie modellata sul framework. Nessuno ci riesce completamente. I tre approcci che dominano il mercato nel 2026 — React Native, Flutter e Kotlin Multiplatform con Compose Multiplatform — rappresentano ognuno un compromesso leggermente diverso tra astrazione e perdita di fedeltà.
accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityState su Pressable e View si mappano quasi 1:1 su UIAccessibility — ma i nomi dei ruoli appartengono al vocabolario React Native, non a quello iOS.accessibilityRole=“button” imposta className su android.widget.Button.accessibilityLiveRegion è solo per Android — su iOS non fa nulla in modo silenzioso, ed è necessario chiamare AccessibilityInfo.announceForAccessibility() manualmente.UIAccessibilityElement sulla Flutter view, con i trait mappati dai set SemanticsAction e SemanticsFlag.SemanticsFlag.isLiveRegion.Modifier.semantics { } di Compose definisce ruoli e azioni una volta sola; ogni target traduce lo stesso blocco di semantica nella propria API a11y nativa.Lo schema è lo stesso in tutti e tre: un albero semantico sintetico modellato sul framework da un lato, due alberi di accessibilità modellati sulla piattaforma dall’altro, e un traduttore in mezzo che gestisce bene i casi semplici e quelli complessi con una perdita di fedeltà percepibile. I casi semplici — un pulsante con un’etichetta, un’immagine con testo alternativo, un’intestazione — si traducono senza perdita. I casi complessi — un gesto personalizzato a due dita, un grafico i cui elementi dovrebbero formare un gruppo focalizzabile, una live region che deve scattare su iOS senza un’impostazione di cortesia associata alla view — fanno emergere nel codice multipiattaforma il vocabolario della piattaforma sottostante, o semplicemente non si traducono.
«Il primo 80 percento dell’accessibilità mobile è identico in ogni framework. L’ultimo 20 percento è dove ogni framework rivela a quale API nativa pensa in segreto.»
4. Il problema WebView — quando l’accessibilità web su mobile fallisce silenziosamente
Sia iOS che Android eseguono il rendering dei contenuti web tramite una WebView di sistema — WKWebView su iOS, android.webkit.WebView (o Chrome Custom Tabs) su Android. In entrambi i casi, la WebView è una scatola nera dal punto di vista dell’app host: l’app vede una singola view, ma il lettore di schermo vede l’intero albero di accessibilità del DOM al suo interno. Il bridge tra i due alberi è il punto in cui una quantità sorprendente di accessibilità mobile su dispositivo va silenziosamente storta.
Il meccanismo è, in apparenza, semplice. Quando il focus di un lettore di schermo entra in una WebView, la piattaforma legge l’albero di accessibilità del documento direttamente dal motore del browser — WebKit su iOS, Blink su Android — e lo attraversa come un sottoalbero dell’albero dell’app host. I ruoli, le etichette e gli attributi ARIA del web vengono tradotti nel vocabolario della piattaforma in tempo reale. Un elemento button senza ruolo esplicito nella WebView viene letto come pulsante su entrambe le piattaforme; una regione aria-live=“polite” annuncia correttamente su entrambe; un aria-label su un link emerge come etichetta di accessibilità del link. Per i primi tre anni di vita del web mobile, questo funzionava semplicemente.
Il problema si manifesta in tre punti. In primo luogo, i gesti personalizzati definiti nell’app host — uno swipe a due dita per chiudere, un magic tap per riprodurre e mettere in pausa — sono invisibili al contenuto della WebView; si attivano sul target sbagliato o non si attivano affatto quando il focus è all’interno del documento. In secondo luogo, i UIAccessibilityElement dell’app host disegnati sopra la WebView (un pulsante di azione mobile, una toolbar personalizzata) competono con l’albero della WebView per l’ordine di attraversamento, e l’ordine di lettura risultante è non deterministico tra le versioni di iOS. In terzo luogo — e questa è la singola modalità di errore più grande nell’accessibilità web su mobile — la WebView su iOS non rispetta i livelli di cortesia di aria-live come fa Safari in una scheda: il plumbing degli annunci di WKWebView elimina la distinzione tra polite e assertive, così ogni aggiornamento live viene trattato come polite indipendentemente dal markup.
<div role="alert" aria-live="assertive">
Connessione persa — nuovo tentativo in corso.
</div>VoiceOver in una normale scheda Safari interrompe l’enunciazione corrente e pronuncia immediatamente il messaggio. La cortesia assertive viene rispettata end-to-end tramite WebKit.
<div role="alert" aria-live="assertive">
Connessione persa — nuovo tentativo in corso.
</div>Stesso markup, stesso motore browser — ma il bridge di accessibilità della WKWebView verso UIKit degrada l’annuncio a un messaggio polite differito. L’utente lo sente dopo un ritardo, a volte dopo aver già digitato nel modulo ora interrotto.
Per gli annunci all’interno di una WebView, l’unico pattern multipiattaforma affidabile nel 2026 è esporre un bridge JavaScript verso l’app host — un piccolo gestore postMessage — e instradare gli annunci assertivi fuori dal DOM, nell’app host, e di ritorno tramite UIAccessibility.post(notification: .announcement, …) su iOS o announceForAccessibility() su Android. Il aria-live del web sopravvive solo per i messaggi genuinamente educati dove qualche secondo di latenza è accettabile.
5. La tabella di mappatura — cosa corrisponde a cosa
Abbiamo mappato 28 primitive che VoiceOver e TalkBack consumano concretamente nella pratica — l’unione della superficie del protocollo iOS UIAccessibility, della superficie Android AccessibilityNodeInfo e delle prop multipiattaforma più usate di React Native e Flutter. La tabella seguente cattura solo le righe contestate: le primitive in cui la mappatura è incompleta, asimmetrica o sorprendente. Le righe con mappatura pulita (etichetta, ruolo pulsante, ruolo immagine, intestazione) sono state omesse per brevità.
| Capacità | iOS UIAccessibility | Android AccessibilityNodeInfo | React Native 0.76 | Flutter 3.27 |
|---|---|---|---|---|
| Testo hint (spiegazione lunga) | accessibilityHint | tooltipText (API 28+) | accessibilityHint (solo iOS) | SemanticsProperties.hint |
| Cortesia live region | N/A — solo post imperativo | setAccessibilityLiveRegion() | accessibilityLiveRegion (solo Android) | SemanticsFlag.isLiveRegion |
| Nascondi sottoalbero all’a11y | accessibilityElementsHidden (solo figli) | importantForAccessibility=“noHideDescendants” | accessibilityElementsHidden / importantForAccessibility | Widget ExcludeSemantics |
| Azione personalizzata (rotore / menu) | UIAccessibilityCustomAction | AccessibilityNodeInfo.AccessibilityAction | accessibilityActions + onAccessibilityAction | SemanticsAction con etichetta personalizzata |
| Semantica adjustable / slider | UIAccessibilityTraitAdjustable + accessibilityIncrement | RangeInfo + ACTION_SCROLL_FORWARD | accessibilityRole=“adjustable” + gestori | Slider espone SemanticsAction.increase |
| Livello intestazione | UIAccessibilityTraitHeader (senza livello) | setHeading(true) (senza livello) | accessibilityRole=“header” (senza livello) | SemanticsProperties.headingLevel (1–6) |
| Stato selezionato / attivato | UIAccessibilityTraitSelected | setSelected(true) + setCheckable() | accessibilityState={selected, checked} | SemanticsFlag.isSelected |
| Semantica gruppo / container | shouldGroupAccessibilityChildren | setScreenReaderFocusable(true) | accessible={true} sul genitore | Widget MergeSemantics |
| Annuncio messaggio una-tantum | UIAccessibility.post(.announcement, …) | view.announceForAccessibility() | AccessibilityInfo.announceForAccessibility() | SemanticsService.announce() |
Tre pattern emergono dalla tabella. In primo luogo, l’asimmetria attorno alle live region è la singola fonte più grande di divergenza multipiattaforma — Android ha un’impostazione di cortesia per view, iOS ha solo un post imperativo globale, e ogni framework sopra è costretto a nascondere la differenza. In secondo luogo, i livelli di intestazione sono l’unico punto in cui Flutter migliora genuinamente rispetto a entrambe le piattaforme native; le primitive iOS e Android sanno solo «questo è un’intestazione», non «questo è un H3 sotto un H2». In terzo luogo, la primitiva «nascondi dall’accessibilità» è più flessibile su Android che su iOS — noHideDescendants nasconde un intero sottoalbero in una mossa sola, mentre iOS richiede di nascondere i figli di ogni container individualmente.
6. Il playbook mobile-native
Impara il vocabolario nativo prima di quello del framework
Ogni bridge multipiattaforma — React Native, Flutter, Compose Multiplatform — ha la propria denominazione per le prop di accessibilità, e ognuno di quei nomi è una leggera menzogna su ciò che la piattaforma sottostante fa realmente. Quando un lettore di schermo non annuncia correttamente, il bug risiede quasi sempre nell’API nativa a cui il framework ha tradotto, non nella prop del framework che si è impostata. Leggere i documenti UIAccessibility e i documenti AccessibilityNodeInfo almeno una volta; i documenti del framework hanno senso solo dopo.
Testa gli annunci live specificamente su iOS
L’asimmetria delle live region dalla sezione 2 significa che qualsiasi codice che assume aria-live=“assertive” o accessibilityLiveRegion=“assertive” funzioni si degraderà silenziosamente su iOS. Costruire un piccolo test harness che attivi sia un annuncio educato che uno assertivo su entrambe le piattaforme, con VoiceOver e TalkBack su dispositivi reali, prima di rilasciare qualsiasi funzionalità la cui UX dipenda dall’udire un cambiamento di stato.
Usa il bridge fuori dalle WebView per tutto ciò che è assertivo
Il declassamento degli annunci assertivi da parte di WKWebView non è un bug che Apple correggerà presto — è rimasto lo stesso in ogni release di iOS dalla 14 in poi. Se si rilascia un’app ibrida in cui l’utente può incontrare un errore fatale all’interno di una WebView, si instrada l’annuncio tramite un bridge JS verso l’host e si lascia che l’host attivi l’annuncio della piattaforma. Il solo web non è sufficiente.
Usa la semantica «merge» o «group» del framework, non figlio per figlio
Sia iOS (shouldGroupAccessibilityChildren), Android (setScreenReaderFocusable) che Flutter (MergeSemantics) forniscono un modo per collassare un cluster visivo — un’icona più un’etichetta più un valore — in un singolo elemento di accessibilità. Usarlo. Il comportamento predefinito «ogni foglia è un elemento focalizzabile» trasforma un chip di navigazione a sei elementi in sei swipe di VoiceOver.
Effettua l’audit con Accessibility Inspector e TalkBack Developer Settings
Entrambe le piattaforme includono un inspector ufficiale gratuito per l’albero di accessibilità live — Accessibility Inspector su macOS (abbinato al simulatore o dispositivo iOS connesso), e l’overlay «Show accessibility focus» più «Developer settings» su Android. Usarli per leggere l’albero della propria app come lo vede il lettore di schermo; non si assume che il debug logging del framework mostri la stessa cosa che la piattaforma mostra a TalkBack.
Conclusione: il framework è a valle della piattaforma
È allettante credere — e la documentazione dei framework incoraggia questa convinzione — che un’API di accessibilità multipiattaforma sia un’unica astrazione unificata su due API native equivalenti. La tabella di mappatura nella sezione 5 smentisce l’unificazione. Le due API native sono state progettate in modo indipendente, da due team diversi, attorno a due modelli mentali diversi su come il lettore di schermo dovrebbe attraversare un documento; le differenze sono reali, emergono attraverso ogni framework, e si manifestano nelle parti dell’esperienza utente che contano di più — aggiornamenti live, gesti personalizzati, sottoalberi nascosti, gerarchie di intestazioni.
La buona notizia, dopo quel paragrafo: le basi si mappano. Un pulsante con un’etichetta, un’immagine con testo alternativo, un’intestazione in cima a una sezione — questi si traducono attraverso ogni framework e vengono annunciati correttamente su entrambe le piattaforme. Se si rilasciano solo quelle primitive, non è necessario pensare a UIAccessibility o AccessibilityNodeInfo; le impostazioni predefinite del framework sono oneste. Il problema inizia quando l’interfaccia utente comincia a fare qualcosa di interessante, che è anche quando l’accessibilità inizia a contare di più.
Il playbook nella sezione 6 è la versione più breve dell’argomento che porta il maggior numero di utenti con disabilità a un’esperienza funzionante: pensare prima alle primitive native, testare su dispositivi reali su entrambe le piattaforme, uscire dalle WebView quando si vuole essere assertivi, raggruppare deliberatamente i nodi foglia e usare gli inspector ufficiali. Il framework scelto aiuta con l’80 percento iniziale e si toglie di mezzo per l’ultimo 20 percento. Quell’ultimo 20 percento è dove vive l’utente di lettore di schermo.
«VoiceOver e TalkBack stanno leggendo due documenti diversi dallo stesso codice sorgente. Se l’utente nota la differenza è una misura di quanto bene si è compresa la piattaforma sotto il proprio framework.»