Mobila inbyggda tillgänglighets-API:er 2026:
UIAccessibility, AccessibilityNode och webben
iOS och Android exponerar båda ett fullt utrustat tillgänglighetsträd till plattformsskärmläsaren — och de två träden är inte överens om något utöver grunderna för etikett och roll. Vi kartlade varje primitiv som VoiceOver och TalkBack faktiskt konsumerar 2026, hur React Native, Flutter och Kotlin Multiplatform försöker brygga dem, och det ställe där mobilwebbtillgängligheten tyst rasar av en klippa.
1. iOS UIAccessibility — etiketter, traits, tips, värden
Allt synligt på en iOS-skärm har, eller kan ha, en tillgänglighetsbeskrivning. Apple levererar den beskrivningen via det informella protokollet UIAccessibility, implementerat av UIView och varje systemkontroll, och via UIAccessibilityElement, en lättviktsklass man allokerar för de delar av gränssnittet som ritas men inte i sig är vyer — tecken i ett egentritat diagram, glyfer inuti en Core Graphics-scen, regioner inuti ett CALayer. VoiceOver, Switch Control, Full Keyboard Access och Voice Control konsumerar alla samma protokoll; att lära sig det en gång ger dig fyra hjälpmedel.
Protokollet exponerar fyra primitiver som spelar roll för nästan varje skärm. Tillgänglighetsetiketten (label) är det korta, läsbara namnet på elementet — “Skicka”, “Profilfoto av Asha”, “Tillbaka”. Tillgänglighetstraits (traits) är en bitmask av rollliknande flaggor — .button, .header, .image, .selected, .adjustable, .staticText, .updatesFrequently — som talar om för VoiceOver hur det ska bete sig kring elementet och vilka gester som ska aktiveras. Tillgänglighetsvärdet (value) är en strängrepresentation av det aktuella tillståndet (“På”, “75%”, “Torsdag 22 maj”). Tillgänglighetstipset (hint) är den längre, valfria förklaringen (“Dubbeltryck för att öppna fotovyn”) som VoiceOver talar efter en fördröjning om användaren inte agerar på etiketten ensam.
De fyra primitiverna komponeras. En knapp läses som etikett + trait + värde: “Wi-Fi, knapp, På”. En reglage läses som etikett + trait + värde + tips: “Volym, justerbar, 60 procent, svep uppåt eller nedåt för att justera”. En egentritat diagramstapel läses som en kedja av UIAccessibilityElements, var och en med en etikett, ett värde och en ram inuti sin behållare. Kedjan är API-ytan — VoiceOver går igenom den linjärt när användaren sveper höger, och respekterar den ordning i vilken du publicerar elementen via behållarens accessibilityElements-array.
Vymodifierarna .accessibilityLabel(), .accessibilityValue(), .accessibilityHint() och .accessibilityAddTraits() i SwiftUI kompileras ned till samma UIAccessibility-egenskaper på den underliggande UIView. SwiftUI lägger också till .accessibilityElement(children:), som löser “tecken i ett diagram”-problemet mer deklarativt än UIKit-metoden — men det körtidskontrakt VoiceOver ser är identiskt. Att lära sig UIKit-namnen är fortfarande värt din tid, eftersom alla Apples exempel, alla Stack Overflow-svar och alla tillgänglighetsgranskningar talar i dem.
2. Android AccessibilityNodeInfo — roller, åtgärder, importantForAccessibility
Android tar en annan väg. Där iOS hänger tillgänglighet på ett platt protokoll på varje vy, serialiserar Android hela tillgänglighetsträdet som ett diagram av AccessibilityNodeInfo-objekt, vart och ett en ögonblicksbild av en vy i det ögonblick en TalkBack-förfrågan anländer. Ramverket konstruerar ögonblicksbilderna lazily; en View publicerar sin nod genom att åsidosätta onInitializeAccessibilityNodeInfo() (eller, i Compose, genom att ange semantikmodifierare), och plattformen sy ihop förälder-barn-relationerna till ett träd som speglar vyhierarkin.
Primitiverna skiljer sig från iOS på tre meningsfulla sätt. För det första exponerar Android en roll via ett strängtypat className-fält — android.widget.Button, android.widget.CheckBox, android.widget.EditText. TalkBack läser klassnamnet och bestämmer hur det ska annonseras (“knapp”, “kryssruta”, “textruta”). Compose översätter sin Role.Button-, Role.Checkbox-, Role.RadioButton-semantik till samma fält. Rollen är mer detaljerad än en iOS-trait-bitmask, men också mer rigid — det finns ingen “helt anpassad” roll om du inte accepterar annonseringen som “vy”.
För det andra representerar Android interaktivitet som en uppsättning åtgärder kopplade till noden: ACTION_CLICK, ACTION_LONG_CLICK, ACTION_SCROLL_FORWARD, ACTION_SET_TEXT, ACTION_FOCUS, och en lång lista av anpassade åtgärder du kan registrera med AccessibilityNodeInfo.AccessibilityAction. TalkBack exponerar de anpassade åtgärderna via “åtgärder”-rotorn — användaren sveper uppåt med ett finger och hör varje anpassad åtgärd vid namn. iOS har samma koncept (UIAccessibilityCustomAction), men på Android är åtgärdslistan ytan; på iOS är gestvokabulären det.
För det tredje har Android importantForAccessibility, en per-vy-enum (auto, yes, no, noHideDescendants) som kontrollerar om noden visas i trädet alls. noHideDescendants är det enskilt kraftfullaste verktyget i Android-tillgänglighet och det som oftast glöms bort — det tar bort hela underträdet från TalkBacks genomgång, motsvarigheten till aria-hidden=“true” på webben. iOS har ingen exakt analog; det närmaste är att ställa accessibilityElements till en tom array på behållaren, vilket bara tar bort behållarens direkta barn, inte hela underträdet.
Android exponerar ViewCompat.setAccessibilityLiveRegion() med tre värden: none, polite, assertive. Vokabulären speglar ARIA — nästan. TalkBack respekterar artighetsnivåerna tillförlitligt. iOS har inget jämförbart på protokollnivå: du annonserar uppdateringar genom att anropa UIAccessibility.post(notification: .announcement, argument: “Sparat”), en imperativ engångspuls som inte kopplas till en vy. Plattformsoberoende bryggor måste fejka en av dessa ovanpå den andra, och impedansobalansen syns i varje ramverk som granskas i avsnitt 3.
3. Plattformsoberoende bryggor — React Native, Flutter, Kotlin Multiplatform
Varje plattformsoberoende mobilramverk måste ta de två API:erna ovan och presentera en enda, ramverksformad yta. Inget av dem lyckas helt. De tre metoderna dominerar marknaden 2026 — React Native, Flutter och Kotlin Multiplatform med Compose Multiplatform — var och en ett lite annorlunda kompromiss mellan läckage och abstraktion.
accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityState på Pressable och View mappas nästan 1:1 till UIAccessibility — men rollnamnen är React Natives vokabulär, inte iOS-vokabulären.accessibilityRole=“button” sätter className till android.widget.Button.accessibilityLiveRegion är Android-only — på iOS gör den tyst ingenting, och du måste anropa AccessibilityInfo.announceForAccessibility() manuellt.UIAccessibilityElement-instanser på Flutter-vyn, med traits mappade från SemanticsAction- och SemanticsFlag-uppsättningarna.SemanticsFlag.isLiveRegion.Modifier.semantics { } definierar roller och åtgärder en gång; varje mål översätter samma semantikblock till sitt eget inbyggda tillgänglighets-API.Mönstret över alla tre är detsamma: ett syntetiskt, ramverksformat semantikträd på ena sidan, två plattformsformade tillgänglighetsträd på den andra, och en översättare däremellan som hanterar enkla fall väl och komplexa fall med ett märkbart fidelitetsförlust. De enkla fallen — en knapp med en etikett, en bild med alternativtext, en rubrik — passerar utan förlust. De komplexa fallen — en anpassad gest med tvåfingersvep, ett diagram vars element bör vara en fokusbar grupp, en live region som måste utlösas på iOS utan en vyinbunden artighetsinställning — läcker det underliggande plattformens vokabulär upp i den plattformsoberoende koden, eller misslyckas helt enkelt med att översätta.
”De första 80 procenten av mobiltillgänglighet är identisk i varje ramverk. De sista 20 procenten är där varje ramverk avslöjar vilket inbyggt API det i hemlighet tänker i.”
4. WebView-glappet — när mobilwebbtillgängligheten tyst misslyckas
Både iOS och Android renderar webbinnehåll via en systemwebview — WKWebView på iOS, android.webkit.WebView (eller Chrome Custom Tabs) på Android. I varje fall är WebView en svart låda ur värdappens perspektiv: appen ser en enda vy, men skärmläsaren ser hela DOM-tillgänglighetsträdet inuti den. Bryggan mellan de två träden är det ställe där en förvånansvärt stor mängd mobiltillgänglighet tyst går fel.
Mekanismen är, vid första anblick, enkel. När en skärmläsares fokus går in i en WebView läser plattformen dokumentets tillgänglighetsträd direkt från webbläsarmotorn — WebKit på iOS, Blink på Android — och traverserar det som ett underträd till värdappens träd. Webbens roller, etiketter och ARIA-attribut översätts till plattformens vokabulär i realtid. Ett button-element utan explicit roll inuti WebView läses som en knapp på båda plattformarna; en aria-live=“polite”-region annonserar korrekt på båda; ett aria-label på en länk framträder som länkens tillgänglighetsetikett. Under mobilwebbens första tre år fungerade detta bara.
Klippan dyker upp på tre ställen. Först: anpassade gester definierade i värdappen — ett tvåfingersvep för att stänga, ett magiknapp-tryck för att spela upp och pausa — är osynliga för WebViewens innehåll; de utlöses på fel mål eller utlöses inte alls när fokus är inuti dokumentet. Andra: värdappens UIAccessibilityElements ritade ovanpå WebView (en flytande åtgärdsknapp, en anpassad verktygsfält) konkurrerar med WebViewens träd om traverseringsordning, och den resulterande läsordningen är icke-deterministisk mellan iOS-versioner. Tredje — och detta är det enskilt största felläget i mobilwebbtillgänglighet — WebView på iOS respekterar inte aria-live-artighetsnivåer på det sätt Safari gör i en flik: WKWebViews annonseringssystem tappar distinktionen mellan polite och assertive, så varje live-uppdatering behandlas som polite oavsett markup.
<div role="alert" aria-live="assertive">
Anslutningen bröts — försöker igen.
</div>VoiceOver i en vanlig Safari-flik avbryter det aktuella yttrandet och talar meddelandet omedelbart. Artigheten assertive respekteras hela vägen genom WebKit.
<div role="alert" aria-live="assertive">
Anslutningen bröts — försöker igen.
</div>Samma markup, samma webbläsarmotor — men WKWebViews tillgänglighetsbrygga till UIKit degraderar annonseringen till ett uppskjutet polite-meddelande. Användaren hör det med fördröjning, ibland efter att de redan har skrivit i det nu-trasiga formuläret.
För annonseringar inuti en WebView är det enda pålitliga plattformsoberoende mönstret 2026 att exponera en JavaScript-brygga in i värdappen — en liten postMessage-hanterare — och routa assertiva annonseringar ut ur DOM, in i värdappen och tillbaka via UIAccessibility.post(notification: .announcement, …) på iOS eller announceForAccessibility() på Android. Webbens aria-live överlever bara för genuint artiga meddelanden där några sekunders latens är acceptabelt.
5. Mappningstabellen — vad som motsvarar vad
Vi kartlade 28 primitiver som VoiceOver och TalkBack faktiskt konsumerar i praktiken — unionen av iOS UIAccessibility-protokollytan, Android AccessibilityNodeInfo-ytan och de mest använda React Native- och Flutter-plattformsoberoende propsarna. Tabellen nedan fångar bara de omstridda raderna: de primitiver där mappningen är ofullständig, asymmetrisk eller överraskande. Rader där mappningen är ren (etikett, knapproll, bildroll, rubrik) har utelämnats för längdens skull.
| Förmåga | iOS UIAccessibility | Android AccessibilityNodeInfo | React Native 0.76 | Flutter 3.27 |
|---|---|---|---|---|
| Tipstexter (längre förklaring) | accessibilityHint | tooltipText (API 28+) | accessibilityHint (iOS-only) | SemanticsProperties.hint |
| Live region-artighet | N/A — bara imperativ post | setAccessibilityLiveRegion() | accessibilityLiveRegion (Android-only) | SemanticsFlag.isLiveRegion |
| Dölj underträd från tillgänglighet | accessibilityElementsHidden (bara barn) | importantForAccessibility=“noHideDescendants” | accessibilityElementsHidden / importantForAccessibility | ExcludeSemantics-widget |
| Anpassad åtgärd (rotor / meny) | UIAccessibilityCustomAction | AccessibilityNodeInfo.AccessibilityAction | accessibilityActions + onAccessibilityAction | SemanticsAction med anpassad etikett |
| Justerbar / reglagsemantik | UIAccessibilityTraitAdjustable + accessibilityIncrement | RangeInfo + ACTION_SCROLL_FORWARD | accessibilityRole=“adjustable” + handlers | Slider exponerar SemanticsAction.increase |
| Rubriknivå | UIAccessibilityTraitHeader (ingen nivå) | setHeading(true) (ingen nivå) | accessibilityRole=“header” (ingen nivå) | SemanticsProperties.headingLevel (1–6) |
| Valt / växlat tillstånd | UIAccessibilityTraitSelected | setSelected(true) + setCheckable() | accessibilityState={selected, checked} | SemanticsFlag.isSelected |
| Grupp / behållarsemantik | shouldGroupAccessibilityChildren | setScreenReaderFocusable(true) | accessible={true} på förälder | MergeSemantics-widget |
| Annonsera engångsmeddelande | UIAccessibility.post(.announcement, …) | view.announceForAccessibility() | AccessibilityInfo.announceForAccessibility() | SemanticsService.announce() |
Tre mönster hoppar ut ur tabellen. Först är asymmetrin kring live regions den enskilt största källan till plattformsoberoende divergens — Android har en per-vy-artighetsinställning, iOS har bara en global imperativ post, och varje ramverk ovan tvingas ljuga om skillnaden. Andra är rubriknivåer det enda stället Flutter genuint förbättrar på båda inbyggda plattformar; iOS- och Android-primitiverna vet bara “det här är en rubrik”, inte “det här är en H3 under en H2”. Tredje är primitiven “dölj från tillgänglighet” mer flexibel på Android än på iOS — noHideDescendants döljer ett helt underträd i ett steg, medan iOS kräver att du döljer varje behållares barn individuellt.
6. Handboken för mobil-native
Lär dig den inbyggda vokabulären före ramverksvokabulären
Varje plattformsoberoende brygga — React Native, Flutter, Compose Multiplatform — har egna namn på tillgänglighets-props, och vart och ett av dessa namn är en liten lögn om vad den underliggande plattformen faktiskt gör. När en skärmläsare inte annonserar korrekt bor buggen nästan alltid i det inbyggda API:et som ramverket översatte till, inte i ramverkets prop du satte. Läs UIAccessibility-dokumentationen och AccessibilityNodeInfo-dokumentationen minst en gång; ramverksdokumentationen blir begriplig först efteråt.
Testa live-annonseringar på iOS specifikt
Live region-asymmetrin från avsnitt 2 innebär att all kod som förutsätter att aria-live=“assertive” eller accessibilityLiveRegion=“assertive” fungerar tyst kommer att degraderas på iOS. Bygg en liten testharness som utlöser både en artig och en assertiv annonsering på båda plattformarna, med VoiceOver och TalkBack på riktiga enheter, innan du lanserar någon funktion vars UX är beroende av att användaren hör en tillståndsförändring.
Brygga ut ur WebViews för allt assertivt
WKWebViews degradering av assertiva annonseringar är inte ett fel Apple kommer att åtgärda snart — det har varit likadant i varje iOS-release från 14 och framåt. Om du levererar en hybridapp där användaren kan stöta på ett allvarligt fel inuti en WebView, routa annonseringen via en JS-brygga till värden och låt värden utlösa plattformens annonsering. Webben ensam räcker inte.
Använd ramverkets “merge”- eller “group”-semantik, inte barn-för-barn
Både iOS (shouldGroupAccessibilityChildren), Android (setScreenReaderFocusable) och Flutter (MergeSemantics) erbjuder ett sätt att komprimera ett visuellt kluster — en ikon plus en etikett plus ett värde — till ett enda tillgänglighetselement. Använd det. Standardbeteendet “varje lövnod är ett fokuserbart element” förvandlar ett sexelements navigeringschip till sex VoiceOver-svep.
Granska med Accessibility Inspector och TalkBack Developer Settings
Båda plattformarna levererar ett gratis, officiellt inspektionsverktyg för det levande tillgänglighetsträdet — Accessibility Inspector på macOS (parat med den anslutna iOS-simulatorn eller enheten), och överlägget “Visa tillgänglighetsfokus” plus “Inställningar för utvecklare” på Android. Använd dem för att läsa din apps träd på det sätt skärmläsaren ser det; anta inte att ramverkets felsökningsloggning visar dig samma sak som plattformen visar TalkBack.
Slutsats: ramverket är nedströms plattformen
Det är frestande att tro — och ramverksdokumentationen uppmuntrar denna tro — att ett plattformsoberoende tillgänglighets-API är en enda, enhetlig abstraktion över två likvärdiga inbyggda API:er. Mappningstabellen i avsnitt 5 motbevisar enhetsuppfattningen. De två inbyggda API:erna designades oberoende av två olika team, kring två olika mentala modeller för hur skärmläsaren ska gå igenom ett dokument; skillnaderna är verkliga, de läcker genom varje ramverk, och läckaget syns i de delar av användarupplevelsen som spelar mest roll — live-uppdateringar, anpassade gester, dolda underträd, rubrikhierarkier.
Den goda nyheten, efter det stycket: grunderna mappas. En knapp med en etikett, en bild med alternativtext, en rubrik högst upp i ett avsnitt — de passerar genom varje ramverk och annonseras korrekt på båda plattformarna. Om du bara levererar dessa primitiver behöver du inte tänka på UIAccessibility eller AccessibilityNodeInfo; ramverkets standardvärden är ärliga. Problemen börjar när användargränssnittet börjar göra något intressant, vilket också är när tillgängligheten börjar spela störst roll.
Handboken i avsnitt 6 är den kortaste versionen av argumentet som ger flest användare med funktionsnedsättning en fungerande upplevelse: tänk i inbyggda primitiver först, testa på riktiga enheter på båda plattformarna, brygga ut ur WebViews när du menar det, gruppera lövnoder medvetet och använd de officiella inspektionsverktygen. Ramverket du valt hjälper med de första 80 procenten och håller sig undan för de sista 20 procenten. De sista 20 procenten är där skärmläsaranvändaren bor.
”VoiceOver och TalkBack läser två olika dokument från samma källkod. Om användaren märker skillnaden är ett mått på hur väl du förstod plattformen under ditt ramverk.”