Mobile Accessibility: iOS, Android & Mobile Web
Mobile accessibility is not desktop accessibility on a small screen, and it is not just “turn on a screen reader.” It is three surfaces — native iOS, native Android, and mobile web — that share one set of user needs through three different APIs, plus a cluster of WCAG criteria that only break on touch and small screens. This guide covers touch targets, gestures, orientation and text scaling, native iOS and Android code, mobile web, and a real-device testing workflow — mapped to WCAG 2.2 AA.
Why Mobile Accessibility Is Different
The user needs are identical everywhere: a blind person using a screen reader, someone who enlarges text, a person who taps with a knuckle or a switch, a user with a tremor. What changes on mobile is how you satisfy those needs. On the web, the browser turns your HTML into an accessibility tree automatically — a real <button> is announced as a button because the platform already knows what a button is. In a native app there is no HTML and no browser doing that translation: the accessibility tree is built by hand, and every control's name, role, value, and state is something you set through the platform accessibility API. Miss it, and VoiceOver or TalkBack reads nothing.
Mobile also adds a whole cluster of failure modes that barely exist on desktop, because the input is a finger on a small screen you can rotate and that has a system-wide text-size setting:
- Touch targets too small to hit reliably (2.5.8).
- Gestures — swipe, pinch, drag — that a screen reader user or a one-handed user cannot perform (2.5.1, 2.5.2).
- Orientation locked to portrait, breaking anyone whose phone is mounted in landscape (1.3.4).
- Text scaling that ignores Dynamic Type or the Android font-size setting (1.4.4), and layouts that will not reflow (1.4.10).
One more thing that trips teams up: WCAG was written for web content, so people assume it does not cover their native app. It does. The W3C publishes WCAG2ICT, a Group Note that explains how to read each success criterion for non-web software — you substitute “software” for “web page” and the criteria still apply. The ADA, the European Accessibility Act, and Section 508 all reach mobile apps, and WCAG 2.2 AA is the standard they are measured against. This guide is the build layer; for how to scope an audit of a native app, see our companion coverage of WCAG-EM and WCAG2ICT.
The WCAG 2.2 Criteria Mobile Breaks Most
| Criterion | Level | What it requires on mobile |
|---|---|---|
| 2.5.1 Pointer Gestures | A | Never require multipoint or path-based gestures; give a single-tap alternative. |
| 2.5.2 Pointer Cancellation | A | Trigger on the up-event, not touch-down, so a user can slide off to abort. |
| 2.5.4 Motion Actuation | A | Shake/tilt features need a UI control and a way to disable motion. |
| 1.3.4 Orientation | AA | Support portrait and landscape unless one is essential. |
| 1.4.4 Resize Text | AA | Text scales to 200% (Dynamic Type / sp units) with no loss of content. |
| 1.4.10 Reflow | AA | Content works at a 320px-wide viewport with no two-dimensional scrolling. |
| 2.5.8 Target Size (Minimum) | AA | Targets at least 24×24 CSS px (aim for 44pt / 48dp); or enough spacing. |
| 1.4.12 Text Spacing | AA | No clipping when line height and letter/word spacing increase. |
| 2.4.7 Focus Visible | AA | Keyboard and switch users see a clear focus indicator on every control. |
| 4.1.2 Name, Role, Value | A | Every control exposes a name, role, and state via the platform API. |
For the full list, see the WCAG 2.2 Level AA requirements and the interactive WCAG 2.2 checklist.
1. Touch Target Size & Spacing
The single most common mobile accessibility failure is a target too small to hit. Two standards apply, and they answer different questions:
- WCAG 2.5.8 Target Size (Minimum), AA — the legal floor: 24×24 CSS pixels, with exceptions for targets that have enough spacing around them, inline links inside a sentence, and cases where the size is essential.
- Platform guidance — the size to actually build to: 44×44 pt (Apple Human Interface Guidelines) and 48×48 dp (Google Material Design).
Treat 24px as the number you must never drop below and 44pt / 48dp as your ergonomic target. Crucially, the tappable area is what counts, not the visible glyph: you can keep a 16px icon and still meet the target by extending the hit region with padding.
/* Mobile web: small icon, real hit area */
.icon-button {
min-width: 44px;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}On Android, wrap a small control in a TouchDelegate or set android:minWidth/minHeight to 48dp. In SwiftUI, standard controls already claim a minimum hit region; for custom shapes add .contentShape(Rectangle())and adequate padding. When two targets sit close together, the spacing exception in 2.5.8 only helps if the gap keeps their 24px hit circles from overlapping — crowded toolbars are the usual offender.
2. Gestures, Pointer Cancellation & Motion
Gestures are where mobile UX and accessibility collide. A blind user's swipes belong to VoiceOver or TalkBack, not to your view; a user with a tremor or one working hand cannot trace a shape or pinch. Three criteria govern this:
2.5.1 Pointer Gestures (A) — if something needs a path-based gesture (swipe to delete, drag to reorder, swipe a carousel) or a multipoint gesture (two-finger pinch to zoom), you must provide a single pointer alternative. Put a delete button behind the swipe, arrow controls on the carousel, and zoom buttons beside the pinch. The gesture is a shortcut, never the only door.
2.5.2 Pointer Cancellation (A) — do not complete an action on the down-event. Fire on the up-event so a user who presses the wrong control can slide a finger away and release harmlessly, and provide an undo where you can. A button that acts the instant a finger lands gives no way to bail out. Native buttons and HTML click already do the right thing; custom touchstart/onPress handlers are where this breaks.
2.5.4 Motion Actuation (A) — if shaking to undo or tilting to scroll drives a feature, offer a normal on-screen control that does the same thing, and let users turn the motion trigger off. Someone whose phone is mounted to a wheelchair cannot shake it, and involuntary movement can fire it by accident. See 2.5.4 Motion Actuation for the full requirement.
For drag operations specifically, WCAG 2.2 added 2.5.7 Dragging Movements (AA): anything you accomplish by dragging must also be doable with single taps. Our accessible slider guide works through a canonical drag control end to end.
3. Orientation, Reflow & Text Scaling
Orientation (1.3.4, AA). Do not lock the screen to portrait or landscape. Many users mount their phone in a fixed orientation on a wheelchair or a stand, or simply prefer one way; locking excludes them unless a specific orientation is truly essential (a piano app, a cheque-scanning camera). On iOS this means not restricting supportedInterfaceOrientations without cause; on Android, avoid a hardcoded android:screenOrientation.
Reflow (1.4.10, AA). Content must work at a viewport 320 CSS pixels wide (equivalent to a 1280px page zoomed to 400%) without the user having to scroll in two directions to read a line. On mobile web that means responsive layout and no fixed-width containers; in native apps it means letting content wrap and grow when text gets bigger, using Auto Layout / constraints rather than fixed frames.
Text scaling (1.4.4, AA). This is the criterion mobile teams miss most, because on the web “resize text” means browser zoom, but on a phone it means the system-wide font-size setting — iOS Dynamic Type and Android font scale, which users can push well past 200%. Hardcoded point or pixel sizes ignore it entirely. Use the platform text styles so type grows with the setting, and test your layouts at the largest size — that is where reflow, clipping, and truncation problems surface. On mobile web, honoring this means never setting user-scalable=no or maximum-scale=1 in the viewport tag (covered in section 6).
4. Native iOS Accessibility (UIKit & SwiftUI)
iOS exposes your UI to VoiceOver, Voice Control, and Switch Control through the UIAccessibilityAPI. The four properties that map onto WCAG's Name, Role, Value are accessibilityLabel (name), accessibilityTraits (role), accessibilityValue (value), and the state carried in the traits. Standard controls come wired up; custom views and icon-only buttons are where you do the work.
UIKit
// An icon-only button with no visible text
let favoriteButton = UIButton(type: .system)
favoriteButton.setImage(UIImage(named: "heart"), for: .normal)
// VoiceOver needs a name, a role, and (optionally) a hint:
favoriteButton.isAccessibilityElement = true
favoriteButton.accessibilityLabel = "Favorite" // name
favoriteButton.accessibilityTraits = .button // role
favoriteButton.accessibilityHint = "Adds this article to your favorites"
// Announce an async change VoiceOver would otherwise miss:
UIAccessibility.post(notification: .announcement,
argument: "Added to favorites")
// Decorative image: take it out of the accessibility tree
decorImageView.isAccessibilityElement = false
// Text must respect the user's Dynamic Type setting:
titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
titleLabel.adjustsFontForContentSizeCategory = trueSwiftUI
SwiftUI generates accessibility from your view tree, so most controls are labeled for free. You add modifiers to name icon-only controls, group related views into a single swipe stop, and hide decoration. System text styles scale with Dynamic Type automatically.
Button(action: toggleFavorite) {
Image(systemName: "heart") // decorative glyph inside the button
}
.accessibilityLabel("Favorite") // name; the .isButton trait is automatic
.accessibilityHint("Adds this article to your favorites")
// Collapse a rating row into ONE element with a combined label:
HStack {
Image(systemName: "star.fill")
Text("4.8")
Text("(120 reviews)")
}
.accessibilityElement(children: .combine) // reads "star, 4.8, 120 reviews" as one stop
// Hide a purely decorative image:
Image("hero-pattern").accessibilityHidden(true)
// System text styles scale with Dynamic Type automatically:
Text("Order summary").font(.headline)Add a role trait when a custom view acts like a control: .accessibilityAddTraits(.isButton), .isHeader, or .isSelected. For values that change, keep accessibilityValuein sync so a slider announces “50 percent,” not just its label. When you scale custom dimensions with the font size, use @ScaledMetric so padding and icons grow alongside the text.
5. Native Android Accessibility (View & Jetpack Compose)
Android surfaces your UI to TalkBack, Switch Access, and Voice Access through the accessibility node tree. In the classic View system you annotate views with XML attributes and delegates; in Jetpack Compose you describe semantics declaratively.
View system
<!-- Icon-only control: give it a name and a 48dp target -->
<ImageButton
android:id="@+id/favorite"
android:src="@drawable/ic_heart"
android:contentDescription="@string/favorite"
android:minWidth="48dp"
android:minHeight="48dp" />
<!-- Associate a visible label with its input -->
<TextView
android:id="@+id/emailLabel"
android:text="@string/email"
android:labelFor="@+id/email" />
<EditText
android:id="@+id/email"
android:textSize="16sp" /> <!-- sp, not dp, so text scales -->
<!-- Decorative image: keep it out of the tree -->
<ImageView
android:src="@drawable/pattern"
android:importantForAccessibility="no" />// Announce an async change to TalkBack
favoriteButton.announceForAccessibility(
getString(R.string.added_to_favorites)
)Jetpack Compose
// Icon-only button: label it; Material enforces a 48dp touch target
IconButton(onClick = ::toggleFavorite) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = "Favorite" // null would mark it decorative
)
}
// Merge a rating row into one node with a combined description:
Row(
modifier = Modifier.semantics(mergeDescendants = true) {}
) {
Icon(Icons.Default.Star, contentDescription = null)
Text("4.8")
Text("(120 reviews)")
}
// A live region announces its own updates, no manual event needed:
Text(
text = statusMessage,
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite }
)
// Text in sp scales with the OS font-size setting:
Text("Order summary", fontSize = 16.sp)Expose role and state through the semantics block — role = Role.Button, stateDescription, selected, toggleableState — and use Modifier.clearAndSetSemanticswhen you need to replace a subtree's auto-generated description with a single clean one. TalkBack, like VoiceOver, moves focus by swipe, so reading order follows the semantics tree; check it matches the visual order.
6. Mobile Web Accessibility
On mobile web the accessibility tree is built for you: write semantic HTML — real <button> and <a href>, labeled inputs, headings, and ARIA only where HTML falls short — and Safari with VoiceOver or Chrome with TalkBack turns it into something a screen reader can navigate. Everything in our keyboard accessibility and accessible forms guides applies. The mobile-web-specific work is smaller and concentrated:
The viewport tag is the number-one mobile-web bug. Disabling zoom to stop your layout from “breaking” is an accessibility failure — low-vision users rely on pinch-zoom.
<!-- Correct: users can pinch-zoom -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Wrong: blocks zoom, fails WCAG 1.4.4 -->
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />Beyond the viewport: give touch targets a real CSS hit area (section 1); make layouts reflow at 320px so nothing needs horizontal scrolling; and show focus with :focus-visible so keyboard and switch users get a clear indicator without it flashing on every tap.
.icon-button:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}Finally, watch hover-only content — there is no hover on touch, so tooltips and menus that only appear on :hover are unreachable. Make them open on tap/focus and stay dismissible, per 1.4.13 Content on Hover or Focus. React Native and Flutter each have their own accessibility APIs that mirror the native ones above (accessibilityLabel/accessibilityRole in React Native, Semantics widgets in Flutter); the same Name/Role/Value discipline carries over.
7. Screen Readers on Mobile: VoiceOver & TalkBack
Mobile screen readers work differently from desktop ones. There is no mouse and no separate keyboard focus — the user explores by touch (drag a finger to hear whatever is under it) or swipes linearly (flick right to the next element, left to the previous), and double-taps anywhere to activate whatever is focused. Because the screen reader owns single-finger swipes, any swipe gesture your UI needs must have a button alternative — this is the concrete reason 2.5.1 exists.
Both screen readers offer a rotor-style control for jumping by headings, links, or form controls, which is exactly why semantic structure matters: without real headings and labels there is nothing to jump between. Test on both platforms, since they expose different bugs.
VoiceOver (iOS)
Enable with Settings → Accessibility → VoiceOver, or triple-click the side button. Swipe to move, double-tap to activate, use the Rotor (two-finger rotate) to change navigation mode. Our VoiceOver testing guide has the full gesture set for iOS and macOS.
TalkBack (Android)
Enable with Settings → Accessibility → TalkBack, or hold both volume keys. Swipe to move, double-tap to activate, swipe up-then-down (or a three-finger swipe) to change reading controls. Our TalkBack testing guide covers the gestures and the mobile-only criteria it exposes.
8. Testing Mobile Accessibility
Use a two-layer workflow: automated tools to catch the mechanical failures fast, then a manual pass on a real device for everything a tool cannot judge. Automated checks find roughly a third of issues; the manual pass finds the rest.
Automated
- iOS: Xcode's Accessibility Inspector (audit a running app for missing labels, small targets, and contrast) and
XCTestaccessibility audits. - Android: the Accessibility Scanner app and the Espresso
AccessibilityChecksintegration in instrumented tests. - Mobile web: axe DevTools and Lighthouse against a mobile viewport — or scan a live URL with our mobile accessibility checker.
Manual, on a real phone
- Turn on the screen reader (VoiceOver / TalkBack) and complete each key task by swiping only — every control should announce a clear name and role.
- Crank the system font size to its maximum and confirm nothing clips, truncates, or overlaps; scroll still reaches everything.
- Rotate the device — the layout should adapt, not lock or break.
- Try every gesture-driven action with a single tap; check for a button alternative.
- Tap near the edges of small controls to feel whether the hit area is really 44pt / 48dp.
Emulators help for a first look, but the gesture feel, real screen-reader focus order, and target ergonomics only show up on hardware.
Common Mobile Mistakes & How to Fix Them
| Anti-pattern | Why it fails | The fix |
|---|---|---|
user-scalable="no" or maximum-scale=1 in the viewport meta tag. | Blocks pinch-zoom, so low-vision users cannot enlarge the page (WCAG 1.4.4). | Use content="width=device-width, initial-scale=1" and let users zoom. |
Icon-only button with no label (no accessibilityLabel / contentDescription / aria-label). | The screen reader announces "button" with no purpose (WCAG 4.1.2, 1.1.1). | Give every icon control a text name; mark purely decorative images as hidden. |
Tap targets smaller than 44pt / 48dp packed together with no spacing. | Users with tremor or large fingers hit the wrong control (WCAG 2.5.8). | Extend the hit area with padding or a TouchDelegate; keep 24px minimum with spacing. |
Swipe, pinch, or drag as the only way to complete an action. | Screen reader and motor-impaired users cannot perform path or multipoint gestures (2.5.1). | Add a single-tap button alternative (arrows, a menu, a stepper) beside the gesture. |
Firing the action on touch-down, or a shake gesture with no alternative. | No way to slide off and abort (2.5.2); motion actuation excludes users who cannot move the device (2.5.4). | Act on the up-event, provide undo, and give motion features a UI control and an off switch. |
Locking the app to portrait and hardcoding font sizes in px/pt. | Breaks users mounted in landscape (1.3.4) and ignores the OS text-size setting (1.4.4). | Support both orientations; use Dynamic Type / sp units so text scales. |
Mobile Accessibility Checklist
- Names on everything. Every control has a name via
accessibilityLabel/contentDescription/aria-labelor a visible label; decorative images are hidden. - Roles & state. Buttons, headers, and selected/checked states are exposed through traits, semantics, or ARIA — not conveyed by visuals alone.
- Touch targets. At least 24×24 CSS px (WCAG floor); build to 44pt (iOS) / 48dp (Android) with real spacing.
- Gestures. No path-based or multipoint gesture is the only way to do something; each has a single-tap alternative.
- Cancellation & motion. Actions fire on the up-event with undo; shake/tilt features have a UI control and an off switch.
- Orientation & reflow. Both orientations supported; content reflows at 320px with no two-dimensional scrolling.
- Text scaling. Type uses Dynamic Type / sp units; on web the viewport allows zoom (no
user-scalable=no). Tested at the largest size. - Focus & reading order. Screen-reader order matches visual order; keyboard/switch focus is visible.
- Announcements. Async changes announce via
UIAccessibility.post/announceForAccessibility/ live region /aria-live. - Real-device pass. Verified with VoiceOver and TalkBack plus the Accessibility Inspector / Scanner, not just an emulator.
Scan a live mobile page with our mobile accessibility checker and work through the full WCAG 2.2 checklist.
Check Your Mobile Experience in Seconds
Run any live page through our free mobile accessibility checker to flag touch-target, viewport, and responsive-layout problems — then work through the native and manual checks above.
Frequently Asked Questions
What is mobile accessibility?▾
Mobile accessibility is the practice of designing and building mobile experiences — native iOS apps, native Android apps, and websites viewed on phones and tablets — so that people who use screen readers, switch access, magnification, large text, voice control, or one hand can complete every task. It shares the same user needs as desktop web accessibility but exposes them through different technology: on the web the browser gives you accessibility semantics for free from HTML, while in a native app there is no HTML, so every control's name, role, value, and state is something you set explicitly through the platform accessibility API. Mobile also introduces failure modes that barely exist on desktop — targets too small to tap, gestures nobody can perform one-handed, screens locked to one orientation, and text that will not grow when the user turns up the system font size.
Does WCAG apply to mobile apps?▾
Yes. The Web Content Accessibility Guidelines were written for web content, but the W3C publishes WCAG2ICT — a Group Note that explains how to apply each WCAG success criterion to non-web software, including native mobile apps. In practice you read a criterion like 4.1.2 Name, Role, Value and substitute "software" for "web page." Regulators treat mobile apps this way too: the Americans with Disabilities Act, the European Accessibility Act, and Section 508 all reach mobile apps, and courts and enforcement bodies routinely cite WCAG 2.1 or 2.2 Level AA as the measuring stick. On top of WCAG you follow the platform guidance — Apple's Human Interface Guidelines and Google's Material Design accessibility guidance — which set stricter, mobile-specific defaults such as minimum touch target size.
What is the minimum touch target size on mobile?▾
There are two numbers and they answer different questions. WCAG 2.2 Success Criterion 2.5.8 Target Size (Minimum) sets a Level AA floor of 24 by 24 CSS pixels, with exceptions for targets that have enough spacing around them, inline targets in a sentence, and targets whose size is essential. The platform guidelines aim higher and are the size you should actually build to: Apple's Human Interface Guidelines call for a minimum tappable area of 44 by 44 points, and Google's Material Design guidance calls for 48 by 48 density-independent pixels. Treat 24px as the legal minimum you must never drop below and 44pt / 48dp as the ergonomic target for anything a person taps with a finger. You can keep an icon visually small while extending its hit area with padding, a TouchDelegate on Android, or SwiftUI's built-in minimum hit region.
How do I make a native iOS app accessible?▾
Turn on VoiceOver (Settings, Accessibility, VoiceOver, or triple-click the side button) and swipe through your screens the way a blind user would. Then fix what you hear. In UIKit, give each meaningful element an accessibilityLabel (the name), the right accessibilityTraits (the role, for example .button or .header), and an accessibilityValue where relevant; mark decorative views isAccessibilityElement = false; and announce asynchronous changes with UIAccessibility.post(notification:). In SwiftUI, standard controls come labeled, so you mostly add .accessibilityLabel, .accessibilityHint, .accessibilityValue, .accessibilityAddTraits, group related views with .accessibilityElement(children:), and hide decoration with .accessibilityHidden(true). Make every text style respect Dynamic Type so labels grow with the user's font-size setting, keep tappable areas at least 44 by 44 points, and support both orientations unless one is genuinely essential.
How do I make a native Android app accessible?▾
Turn on TalkBack (Settings, Accessibility, TalkBack, or the volume-key shortcut) and explore by touch. In the View system, give image-only controls a contentDescription, associate visible labels with inputs using android:labelFor, mark decoration importantForAccessibility="no", size touch targets to at least 48dp, use sp units for text so it scales, and announce changes with view.announceForAccessibility(). In Jetpack Compose, set contentDescription on Icon and Image (null marks them decorative), merge related nodes with Modifier.semantics(mergeDescendants = true), expose role and state through the semantics block, and use LiveRegionMode for content that updates. Compose Material components already enforce a 48dp minimum touch target. Verify with the Accessibility Scanner app and an automated Espresso accessibility check, but always finish with a manual TalkBack pass.
Which WCAG success criteria are specific to mobile?▾
A cluster of criteria fail almost exclusively on touch and small screens: 2.5.1 Pointer Gestures (do not require multipoint or path-based gestures like pinch or swipe-in-a-shape without a simple alternative), 2.5.2 Pointer Cancellation (act on the up-event, not the down-event, so a user can slide off to abort), 2.5.4 Motion Actuation (anything triggered by shaking or tilting the device needs a UI alternative and a way to turn it off), 2.5.8 Target Size Minimum and 2.5.5 Target Size Enhanced, 1.3.4 Orientation (do not lock to portrait or landscape unless essential), 1.4.10 Reflow (content works at a 320px-wide viewport with no two-dimensional scrolling), and 1.4.4 Resize Text (text scales to 200% without loss). Getting this cluster right is most of what separates a mobile-ready product from a desktop site squeezed onto a phone.
Is testing on a real device necessary, or are emulators enough?▾
Emulators and automated scanners are a useful first pass, but they cannot reproduce the real experience. VoiceOver and TalkBack gesture models, focus order under a real screen reader, the feel of a 44pt target under a thumb, and how the layout behaves at the largest Dynamic Type or font-scale setting all need a physical device. The reliable workflow is: run an automated audit (Xcode's Accessibility Inspector, Android's Accessibility Scanner, or axe DevTools and Lighthouse for mobile web) to catch missing names and contrast issues, then do a manual pass on a real phone with the screen reader on, the font size cranked up, and the device rotated. Automated tools find roughly a third of issues; the manual pass finds the rest.
How is mobile web accessibility different from native app accessibility?▾
The underlying WCAG requirements are the same, but the mechanics differ. On mobile web you build accessible HTML — real buttons and links, labeled form fields, headings, and ARIA only where HTML falls short — and the browser plus the screen reader (VoiceOver in Safari, TalkBack in Chrome) turn that into the accessibility tree automatically. Your mobile-web-specific work is mostly the viewport: never set user-scalable=no or maximum-scale=1 (it blocks pinch-zoom and fails 1.4.4), make sure layouts reflow at 320px, give touch targets a real CSS hit area, and use :focus-visible so keyboard and switch users see focus without it flashing on every tap. In a native app there is no browser doing that translation, so you build the accessibility tree by hand with UIAccessibility / SwiftUI modifiers on iOS or contentDescription / Compose semantics on Android.
Essential Accessibility Resources
Comprehensive tools, checklists, and guides to help you create inclusive digital experiences