Mobile-native tilgængeligheds-API’er i 2026:
UIAccessibility, AccessibilityNode og nettet
iOS og Android eksponerer begge et fuldt udbygget tilgængelighed-træ til platformens skærmlæser — og de to træer er ikke enige om noget ud over label-og-rolle-grundlaget. Vi kortlagde alle primitiver, som VoiceOver og TalkBack faktisk forbruger i 2026, den måde React Native, Flutter og Kotlin Multiplatform forsøger at bygge bro, og det sted, hvor mobil WebView-tilgængelighed stille falder af en klippe.
1. iOS UIAccessibility — labels, traits, hints, values
Alt synligt på en iOS-skærm har — eller kan have — en tilgængelighed-repræsentation. Apple leverer den repræsentation via den uformelle protokol UIAccessibility, implementeret af UIView og alle systemkontroller, og via UIAccessibilityElement, en letvægtklasse, man allokerer til de dele af grænsefladen, der er tegnet men ikke i sig selv er views — tegn i et brugerdefineret diagram, glyffer i en Core Graphics-scene, regioner inde i et CALayer. VoiceOver, Switch Control, Full Keyboard Access og Voice Control forbruger alle den samme protokol; at lære den én gang giver adgang til fire hjælpeteknologier.
Protokollen eksponerer fire primitiver, der er vigtige for næsten alle skærme. Tilgængeligheds-labelet er elementets korte, menneskelig-læsbare navn — “Send”, “Profilbillede af Asha”, “Tilbage”. Tilgængeligheds-traits er en bitmask af rollelignende flag — .button, .header, .image, .selected, .adjustable, .staticText, .updatesFrequently — der fortæller VoiceOver, hvordan det skal opføre sig over for elementet, og hvilke gestikker det skal aktivere. Tilgængeligheds-value er en strengrepræsentation af den aktuelle tilstand (“On”, “75%”, “torsdag den 22. maj”). Tilgængeligheds-hintet er den længere, valgfrie forklaring (“Dobbelt-tryk for at åbne fotofremviseren”), som VoiceOver udtaler med forsinkelse, hvis brugeren ikke handler på labelet alene.
De fire primitiver sammensættes. En knap til/fra læses som label + trait + value: “Wi-Fi, afbryderknap, On”. En skyder læses som label + trait + value + hint: “Lydstyrke, justerbar, 60 procent, stryg op eller ned for at justere”. En brugerdefineret diagrambjælke læses som en kæde af UIAccessibilityElementer, hver med et label, en value og en ramme inde i sin container. Kæden er API-fladen — VoiceOver gennemløber den lineært, når brugeren stryger til højre, og respekterer den rækkefølge, hvori elementerne publiceres via containerens accessibilityElements-array.
View-modifikatorerne .accessibilityLabel(), .accessibilityValue(), .accessibilityHint() og .accessibilityAddTraits() i SwiftUI kompilerer ned til de samme UIAccessibility-egenskaber på den underliggende UIView. SwiftUI tilføjer også .accessibilityElement(children:), som løser “tegn i et diagram”-problemet mere deklarativt end UIKit-tilgangen — men den runtime-kontrakt, VoiceOver ser, er identisk. Det er stadig værd at lære UIKit-navnene, fordi alle Apples eksempler, alle Stack Overflow-svar og alle tilgængelighed-audits taler i dem.
2. Android AccessibilityNodeInfo — roller, handlinger, importantForAccessibility
Android tager en anden rute. Hvor iOS hænger tilgængelighed op på en flad protokol på hvert view, serialiserer Android hele tilgængelighed-træet som en graf af AccessibilityNodeInfo-objekter, hvert et snapshot af et view i det øjeblik, en TalkBack-forespørgsel ankommer. Frameworket konstruerer snapshotterne dovent; et View publicerer sin node ved at tilsidesætte onInitializeAccessibilityNodeInfo() (eller, i Compose, ved at sætte semantics-modifikatorer), og platformen syr forældre-barn-relationer til et træ, der spejler view-hierarkiet.
Primitiverne adskiller sig fra iOS på tre meningsfulde måder. For det første eksponerer Android en rolle via et strengtypet className-felt — android.widget.Button, android.widget.CheckBox, android.widget.EditText. TalkBack læser klassenavnet og beslutter, hvordan det skal annonceres (“knap”, “afkrydsningsfelt”, “redigeringsfelt”). Compose oversætter sine Role.Button-, Role.Checkbox-, Role.RadioButton-semantik til det samme felt. Rollen er mere granulær end en iOS trait-bitmask, men også mere rigid — der er ingen “fuldt brugerdefineret” rolle, medmindre man accepterer annonceringen som “view”.
For det andet repræsenterer Android interaktivitet som et sæt handlinger knyttet til noden: ACTION_CLICK, ACTION_LONG_CLICK, ACTION_SCROLL_FORWARD, ACTION_SET_TEXT, ACTION_FOCUS og en lang liste af brugerdefinerede handlinger, man kan registrere med AccessibilityNodeInfo.AccessibilityAction. TalkBack eksponerer de brugerdefinerede handlinger via “handlinger”-rotoren — brugeren stryger op med én finger og hører hver brugerdefineret handling ved navn. iOS har det samme koncept (UIAccessibilityCustomAction), men på Android er handlingslisten fladen; på iOS er gestik-vokabularet det.
For det tredje har Android importantForAccessibility, en per-view-enum (auto, yes, no, noHideDescendants), der styrer, om noden overhovedet optræder i træet. noHideDescendants er det mest kraftfulde enkeltværktøj i Android-tilgængelighed og det, der oftest glemmes — det fjerner hele undertræet fra TalkBacks gennemløb, svarende til aria-hidden=“true” på nettet. iOS har ingen nøjagtig pendant; det nærmeste er at sætte accessibilityElements til et tomt array på containeren, som kun fjerner containerens direkte børn, ikke hele undertræet.
Android eksponerer ViewCompat.setAccessibilityLiveRegion() med tre værdier: none, polite, assertive. Vokabularet spejler ARIA — næsten. TalkBack overholder politeness-niveauerne pålideligt. iOS har intet sammenligneligt på protokolniveauet: opdateringer annonceres ved at kalde UIAccessibility.post(notification: .announcement, argument: “Gemt”), en imperativ én-skudsbesked, der ikke er knyttet til et view. Tværplatformbrygger er nødt til at simulere den ene ovenpå den anden, og impedans-uoverensstemmelsen viser sig i hvert framework gennemgået i afsnit 3.
3. Tværplatformbrygger — React Native, Flutter, Kotlin Multiplatform
Ethvert tværplatform mobilframework er nødt til at tage de to API’er ovenfor og præsentere en enkelt, framework-formet flade. Ingen af dem lykkes fuldstændigt. De tre tilgange dominerer markedet i 2026 — React Native, Flutter og Kotlin Multiplatform med Compose Multiplatform — hver en lidt anden handel mellem læk og abstraktion.
accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityState på Pressable og View mapper næsten 1:1 til UIAccessibility — men rollenavnene er React Native-vokabularet, ikke iOS-vokabularet.accessibilityRole=“button” sætter className til android.widget.Button.accessibilityLiveRegion-propen er kun Android — på iOS gør den stille ingenting, og man er nødt til at kalde AccessibilityInfo.announceForAccessibility() manuelt.UIAccessibilityElement-instanser på Flutter-viewet, med traits mappet fra SemanticsAction- og SemanticsFlag-sæt.SemanticsFlag.isLiveRegion.Modifier.semantics { } definerer roller og handlinger én gang; hvert target oversætter den samme semantics-blok til sit eget native a11y-API.Mønsteret på tværs af alle tre er det samme: et syntetisk, framework-formet semantisk træ på den ene side, to platformformede tilgængelighed-træer på den anden, og en oversætter imellem, der håndterer de simple tilfælde godt og de komplekse med et mærkbart tab af trouvailles. De simple tilfælde — en knap med et label, et billede med alternativ tekst, en overskrift — round-tripper uden tab. De komplekse tilfælde — en brugerdefineret gestik med to-fingersstrygning, et diagram, hvis elementer bør være en fokuserbar gruppe, en live region, der skal aktiveres på iOS uden en view-bundet politeness-indstilling — lækker det underliggende platforms vokabular op i tværplatformkoden eller fejler simpelthen i oversættelsen.
„De første 80 procent af mobiltilgængelighed er identiske på tværs af alle frameworks. De sidste 20 procent er der, hvor hvert framework afslører, hvilket native API det i hemmelighed tænker i.“
4. WebView-kløften — når mobil-webtilgængelighed stille fejler
Både iOS og Android gengiver webindhold via en system-WebView — WKWebView på iOS, android.webkit.WebView (eller Chrome Custom Tabs) på Android. I begge tilfælde er WebView en sort boks fra værtappens perspektiv: appen ser ét view, men skærmlæseren ser hele DOM-tilgængelighed-træet inde i det. Broen mellem de to træer er det sted, hvor en overraskende stor mængde mobiltilgængelighed går stille galt.
Mekanismen er, ved første øjekast, ligetil. Når en skærmlæsers fokus entrer en WebView, læser platformen dokumentets tilgængelighed-træ direkte fra browsermotoren — WebKit på iOS, Blink på Android — og gennemløber det som et undertræ af værtappens træ. Nettets roller, labels og ARIA-attributter oversættes til platformens vokabular i realtid. Et button-element uden eksplicit rolle inde i WebView læses som en knap på begge platforme; en aria-live=“polite”-region annoncerer korrekt på begge; en aria-label på et link fremkommer som linkens tilgængelighed-label. De første tre år af mobilt webliv fungerede dette bare.
Klippen dukker op tre steder. For det første er brugerdefinerede gestikker defineret i værtappen — en to-fingersstrygning for at afvise, et magic-tap for at afspille og sætte på pause — usynlige for WebViewens indhold; de aktiveres på det forkerte mål eller aktiveres slet ikke, når fokus er inde i dokumentet. For det andet konkurrerer værtappens UIAccessibilityElementer tegnet over WebView (en flydende handlingsknap, en brugerdefineret værktøjslinje) med WebViewens træ om gennemløbsrækkefølgen, og den resulterende læserækkefølge er ikke-deterministisk på tværs af iOS-versioner. For det tredje — og dette er den største enkeltfejlmode i mobil-webtilgængelighed — overholder WebView på iOS ikke aria-live politeness-niveauer på den måde, Safari gør på en fane: WKWebViews annonceringssystem dropper skelnen mellem polite og assertive, så alle live-opdateringer behandles som polite uanset markuppet.
<div role="alert" aria-live="assertive">
Forbindelsen mistet — prøver igen.
</div>VoiceOver i en normal Safari-fane afbryder den aktuelle ytring og udtaler beskeden straks. assertive politeness overholdes fra ende til anden via WebKit.
<div role="alert" aria-live="assertive">
Forbindelsen mistet — prøver igen.
</div>Samme markup, samme browsermotor — men WKWebViews tilgængelighed-bro til UIKit nedgraderer annonceringen til en udsat polite-besked. Brugeren hører den med forsinkelse, sommetider efter de allerede har tastet ind i den nu-ødelagte formular.
For annonceringer inde i en WebView er det eneste pålidelige tværplatformmønster i 2026 at eksponere en JavaScript-bro til værtappen — en lille postMessage-handler — og route assertive-annonceringer ud af DOM, ind i værtappen og tilbage via UIAccessibility.post(notification: .announcement, …) på iOS eller announceForAccessibility() på Android. Nettets aria-live overlever kun til genuint høflige beskeder, hvor et par sekunders forsinkelse er acceptabel.
5. Kortlægningstabellen — hvad svarer til hvad
Vi kortlagde 28 primitiver, som VoiceOver og TalkBack faktisk forbruger i praksis — fagforeningen af iOS UIAccessibility-protokolfladen, Android AccessibilityNodeInfo-fladen og de mest brugte React Native- og Flutter-tværplatformprops. Tabellen nedenfor fanger kun de omstridte rækker: de primitiver, hvor kortlægningen er ufuldstændig, asymmetrisk eller overraskende. Rækker, hvor kortlægningen er ren (label, knapperoll, billedrolle, overskrift), er udeladt af pladshensyn.
| Kapabilitet | iOS UIAccessibility | Android AccessibilityNodeInfo | React Native 0.76 | Flutter 3.27 |
|---|---|---|---|---|
| Hint-tekst (længere forklaring) | accessibilityHint | tooltipText (API 28+) | accessibilityHint (kun iOS) | SemanticsProperties.hint |
| Live region politeness | N/A — kun imperativ post | setAccessibilityLiveRegion() | accessibilityLiveRegion (kun Android) | SemanticsFlag.isLiveRegion |
| Skjul undertræ fra a11y | accessibilityElementsHidden (kun børn) | importantForAccessibility=“noHideDescendants” | accessibilityElementsHidden / importantForAccessibility | ExcludeSemantics-widget |
| Brugerdefineret handling (rotor/menu) | UIAccessibilityCustomAction | AccessibilityNodeInfo.AccessibilityAction | accessibilityActions + onAccessibilityAction | SemanticsAction med brugerdefineret label |
| Justerbar/skyder-semantik | UIAccessibilityTraitAdjustable + accessibilityIncrement | RangeInfo + ACTION_SCROLL_FORWARD | accessibilityRole=“adjustable” + handlers | Slider eksponerer SemanticsAction.increase |
| Overskriftsniveau | UIAccessibilityTraitHeader (intet niveau) | setHeading(true) (intet niveau) | accessibilityRole=“header” (intet niveau) | SemanticsProperties.headingLevel (1–6) |
| Valgt/skiftet tilstand | UIAccessibilityTraitSelected | setSelected(true) + setCheckable() | accessibilityState={selected, checked} | SemanticsFlag.isSelected |
| Gruppe/container-semantik | shouldGroupAccessibilityChildren | setScreenReaderFocusable(true) | accessible={true} på forælder | MergeSemantics-widget |
| Annoncer én-skudsbesked | UIAccessibility.post(.announcement, …) | view.announceForAccessibility() | AccessibilityInfo.announceForAccessibility() | SemanticsService.announce() |
Tre mønstre springer frem af tabellen. For det første er asymmetrien omkring live regions den største enkeltårsag til tværplatform-divergens — Android har en per-view politeness-indstilling, iOS har kun en global imperativ post, og alle frameworks ovenfor er tvunget til at lyve om forskellen. For det andet er overskriftsniveauer det ene sted, Flutter faktisk forbedrer på begge native platforme; iOS- og Android-primitiverne ved kun “dette er en overskrift”, ikke “dette er en H3 under en H2”. For det tredje er “skjul fra tilgængelighed”-primitiven mere fleksibel på Android end på iOS — noHideDescendants skjuler et helt undertræ i ét træk, mens iOS kræver, at man skjuler hvert containers børn individuelt.
6. Spillebogen for mobile-native
Lær det native vokabular før framework-vokabularet
Enhver tværplatformsbro — React Native, Flutter, Compose Multiplatform — har sit eget navngivning for tilgængelighed-props, og hvert af disse navne er en lille løgn om, hvad den underliggende platform faktisk gør. Når en skærmlæser ikke annoncerer korrekt, lever fejlen næsten altid i det native API, frameworket oversatte til, ikke i framework-propen man satte. Læs UIAccessibility-dokumentationen og AccessibilityNodeInfo-dokumentationen mindst én gang; framework-dokumenterne giver kun mening bagefter.
Test live-annonceringer på iOS specifikt
Live-region-asymmetrien fra afsnit 2 betyder, at enhver kode, der antager, at aria-live=“assertive” eller accessibilityLiveRegion=“assertive” virker, vil degradere stille på iOS. Byg en lille testramme, der aktiverer både en polite og en assertive annoncering på begge platforme, med VoiceOver og TalkBack på rigtige enheder, inden man leverer en funktion, hvis UX afhænger af, at brugeren hører en tilstandsændring.
Bro ud af WebViews for alt assertive
WKWebViews nedgradering af assertive-annonceringer er ikke en fejl, Apple vil rette snart — det har været det samme i alle iOS-versioner fra 14 og frem. Hvis man leverer en hybrid-app, hvor brugeren kan støde på en fatal fejl inde i en WebView, skal man route annonceringen via en JS-bro til værtappen og lade værtappen aktivere platformsannonceringen. Nettet alene er ikke nok.
Brug frameworkets “merge”- eller “gruppe”-semantik, ikke børn-for-børn
Både iOS (shouldGroupAccessibilityChildren), Android (setScreenReaderFocusable) og Flutter (MergeSemantics) giver en måde at sammensætte et visuelt klynge — et ikon plus et label plus en value — til et enkelt tilgængelighed-element. Brug det. Standardadfærden “hvert blad er et fokuserbart element” gør en seks-element navigationschip til seks VoiceOver-strygninger.
Auditér med Accessibility Inspector og TalkBack Developer Settings
Begge platforme leverer en gratis, officiel inspektor for det live tilgængelighed-træ — Accessibility Inspector på macOS (parret med den tilsluttede iOS-simulator eller -enhed) og “Vis tilgængelighed-fokus” plus “Udviklerindstillinger”-overlayét på Android. Brug dem til at læse sin egen apps træ, som skærmlæseren ser det; antag ikke, at frameworkets fejllog viser det samme, platformen viser TalkBack.
Konklusion: frameworket er nedstrøms for platformen
Det er fristende at tro — og framework-dokumentationen opmuntrer til denne tro — at et tværplatform tilgængeligheds-API er en enkelt, samlet abstraktion over to ækvivalente native API’er. Kortlægningstabellen i afsnit 5 modbeviser foreningen. De to native API’er er designet uafhængigt af to forskellige teams, omkring to forskellige mentale modeller for, hvordan skærmlæseren bør gennemgå et dokument; forskellene er reelle, de lækker igennem alle frameworks, og lækagen viser sig i de dele af brugeroplevelsen, der betyder mest — live-opdateringer, brugerdefinerede gestikker, skjulte undertræer, overskrifthierarkier.
De gode nyheder, efter det afsnit: grundlaget mapper. En knap med et label, et billede med alternativ tekst, en overskrift øverst i et afsnit — de round-tripper igennem alle frameworks og annonceres korrekt på begge platforme. Hvis man kun leverer disse primitiver, behøver man ikke tænke på UIAccessibility eller AccessibilityNodeInfo; frameworkets standardindstillinger er ærlige. Problemerne starter, når brugergrænsefladen begynder at gøre noget interessant, hvilket også er, når tilgængelighed begynder at betyde mest.
Spillebogen i afsnit 6 er den korteste version af argumentet, der får de fleste mennesker med handicap til en fungerende oplevelse: tænk i native primitiver først, test på rigtige enheder på begge platforme, bro ud af WebViews, når det er meningen, grupper bladnoder bevidst og brug de officielle inspektorer. Det framework, man valgte, hjælper med de første 80 procent og holder sig ude af vejen for de sidste 20 procent. De sidste 20 procent er der, hvor skærmlæserbrugeren lever.
„VoiceOver og TalkBack læser to forskellige dokumenter fra den samme kildekode. Om brugeren bemærker forskellen er et mål for, hvor godt man forstod platformen under sit framework.“