Accessibility (A11y) is about making digital experiences usable for everyone, including people with visual, auditory, motor, or cognitive disabilities. It’s the practice of writing code, designing interfaces, and structuring content so that no user is left out, no matter how they interact with technology.
As developers, accessibility isn’t just another checklist — it’s part of building quality software. It means using semantic HTML, managing focus correctly, testing with real assistive tools, and ensuring every component works for all users. Beyond empathy, it also improves performance, SEO, and usability: it’s good for users, good for business, and good for developers who care about clean, future-proof solutions.
Part 1 — Digital Accessibility
Digital accessibility ensures that digital products — websites, mobile apps, documents, and technologies — are designed so that people with disabilities can perceive, understand, navigate, and interact with them effectively.
1.1 Understanding Disability
Disability is diverse and affects people in different ways. Digital accessibility addresses multiple types of disabilities:
- Visual: Blindness, low vision, color blindness
- Auditory: Deafness, hard of hearing
- Motor/Physical: Limited dexterity, tremors, paralysis
- Cognitive: Learning disabilities, memory impairments, attention disorders
- Neurological: Seizure disorders, vestibular disorders
- Speech: Difficulty or inability to speak
Temporary and situational disabilities
1.2 The Global Impact
According to the World Health Organization (WHO):
These numbers are growing as populations age. The business case extends well beyond compliance: accessible products expand market reach, avoid legal risk (ADA, Section 508, EN 301 549, AODA), rank better in search engines, improve usability for every user, strengthen brand reputation, and drive innovation (voice control, captions, dark mode all began as accessibility features).
2. The POUR Principles
WCAG 2.1 (and the newer WCAG 2.2) are organized around four principles, known by the acronym POUR. These apply to every digital platform, web or mobile.
Perceivable
Users must be able to perceive the information being presented.
- Text Alternatives: provide text alternatives for non-text content
- Time-based Media: provide alternatives for audio and video
- Adaptable: content can be presented in different ways without losing information
- Distinguishable: make it easier to see and hear content (contrast, text size, color)
Operable
Users must be able to operate the interface.
- Keyboard Accessible: all functionality available from a keyboard
- Enough Time: users have enough time to read and use content
- Seizures: don’t design content that causes seizures
- Navigable: help users navigate, find content, and know where they are
- Input Modalities: make it easier to operate functionality through various inputs
Understandable
Information and the operation of the UI must be understandable.
- Readable: make text readable and understandable
- Predictable: pages appear and operate in predictable ways
- Input Assistance: help users avoid and correct mistakes
Robust
Content must work with current and future technologies.
- Compatible: maximize compatibility with current and future user agents and assistive technologies
- Valid Code: use valid, semantic code that assistive technologies can parse
- Name, Role, Value: every component has an accessible name, role, and value
3. WCAG Conformance Levels
WCAG defines three levels of conformance, each building on the previous one:
- Level A — the most basic level. Failure to meet Level A creates significant barriers. Essential foundation.
- Level AA — deals with the biggest and most common barriers. The target level for most organizations and often legally required. Recommended baseline for the whole site.
- Level AAA — the highest level. Not required for entire sites, since some content can’t meet AAA. Target it for specific critical content.
Key Level AA requirements you’ll encounter frequently:
- Color Contrast: 4.5:1 for normal text, 3:1 for large text
- Resize Text: resizable up to 200% without loss of content
- Reflow: content reflows to a single column without horizontal scrolling
- Non-text Contrast: 3:1 contrast for UI components
- Text Spacing: no loss of content when text spacing is adjusted
- Focus Visible: the keyboard focus indicator is visible
- Consistent Navigation: navigation stays consistent across pages
4. Assistive Technologies
These technologies bridge users with disabilities and digital content — understanding them helps you build better experiences.
Screen Readers
Screen readers convert digital text into synthesized speech or refreshable Braille: JAWS (Windows, commercial), NVDA (Windows, free), VoiceOver (built into macOS/iOS/iPadOS), TalkBack (Android), and Narrator (Windows). They parse HTML structure and semantics, announce headings/landmarks/links/form controls, allow navigation by element type, and provide a virtual cursor with browse/forms/table navigation modes.
Screen Magnification
ZoomText, the built-in Windows/macOS Magnifier, and browser zoom all enlarge the screen for low-vision users. Design for it: content must reflow at 200% zoom, avoid horizontal scrolling at high zoom, and keep adequate spacing between interactive elements.
Voice Control and Dictation
Dragon NaturallySpeaking, macOS/iOS Voice Control, and Android Voice Access let users navigate by speaking. Use visible labels that match accessible names, give similar elements unique names, and label every interactive element.
Alternative Input Devices
Switch controls, eye tracking, head mice, mouth sticks, and sip-and-puff devices serve users with motor disabilities — most emulate a keyboard. Ensure full keyboard accessibility, target sizes of at least 44×44 px on mobile, generous input time, and avoid (or provide alternatives to) time-based interactions.
5. Testing Methodology
A comprehensive testing strategy includes three layers, and all three are necessary:
- Automated Testing (Foundation): catches 30–40% of issues, runs continuously
- Manual Testing (Core): catches 50–60% of issues, requires expertise
- User Testing (Peak): catches the rest, provides real-world validation
Automated Testing Tools
Browser extensions for manual audits: axe DevTools, WAVE, Lighthouse, and the IBM Equal Access Checker. For CI/CD: Pa11y, axe-core, jest-axe (React/Jest), and cypress-axe (Cypress E2E).
Manual Testing Checklist
- Navigate the entire site using only Tab, Shift+Tab, Enter, Space, and arrow keys
- Confirm the focus indicator is always visible with 3:1 contrast
- Test with NVDA (Windows) or VoiceOver (Mac)
- Zoom to 200% and verify no content loss or horizontal scrolling
- Check all text and UI components meet minimum contrast ratios
- Confirm color isn’t the only way information is conveyed
- Verify form errors are clearly announced and associated with fields
- Check every image has appropriate alt text and every video has accurate captions
- Check the heading hierarchy is logical, with no skipped levels
Screen Reader Testing Commands
- NVDA: Insert+Down Arrow (browse mode), Insert+Space (switch modes)
- VoiceOver: VO+A (read all), VO+Cmd+H (next heading), VO+Cmd+L (next link)
- TalkBack: swipe right (next), swipe left (previous), double-tap (activate)
User Testing with People with Disabilities
Nothing replaces testing with actual users who have disabilities — they surface issues automated tools and expert reviews miss. Recruit users across vision, motor, cognitive, and hearing disabilities; give them real scenarios; let them use their own assistive tech and settings; observe without interrupting; and compensate them fairly. Find testers through Fable (fable.io), Access Works, local disability organizations, or existing customers who use assistive technologies.
Part 2 — Web Accessibility
6. Semantic HTML — The Foundation
Semantic HTML elements clearly describe their meaning to both browsers and assistive technologies. Using the correct elements gives you built-in accessibility (native keyboard support, roles, behaviors), proper screen reader support, better SEO, easier maintainability, and better cross-device compatibility.
Document Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessible Page</title>
</head>
<body>
<header>
<nav aria-label="Main navigation">
<a href="/">Home</a> |
<a href="/about">About</a> |
<a href="/contact">Contact</a>
</nav>
</header>
<main id="main-content">
<h1>Page Title</h1>
<article>
<h2>Article Heading</h2>
<p>Article content goes here...</p>
</article>
<aside aria-label="Related links">
<h2>Related Articles</h2>
<a href="/article1">Article 1</a>
</aside>
</main>
<footer>
<p>© 2025 Company Name</p>
</footer>
</body>
</html>Key semantic elements:
<header>— introductory content or navigation<nav>— navigation links<main>— the main content (use once per page)<article>— self-contained content that could stand alone<section>— a thematic grouping of content with a heading<aside>— content tangentially related to the surrounding content<footer>— footer information
Heading Hierarchy
Headings create a document outline that screen reader users navigate. Use one <h1> per page, never skip levels (h1 → h2 → h3, not h1 → h3), use headings for structure rather than styling, and make sure each heading accurately describes the content that follows.
<!-- ✅ Good Heading Hierarchy -->
<section>
<h1>Introduction to Web Accessibility</h1>
<h2>What is Accessibility?</h2>
<p>Content...</p>
<h3>Types of Disabilities</h3>
<p>Content...</p>
<h3>Benefits of Accessibility</h3>
<p>Content...</p>
<h2>Getting Started</h2>
<p>Content...</p>
</section>
<!-- ❌ Bad Heading Hierarchy: Skipped Levels -->
<section>
<h1>Title</h1>
<h4>Should be h2</h4>
</section>Links and Buttons
Links (<a>) navigate to a different page or location; buttons (<button>) trigger an action on the current page. Use descriptive link text (never “click here”), indicate when a link opens in a new window, distinguish visited from unvisited links, and give links adequate spacing.
<!-- ✅ Accessible icon button -->
<button aria-label="Close dialog">
<span aria-hidden="true">×</span>
</button>
<!-- ✅ Toggle button -->
<button
aria-pressed="false"
onclick="this.setAttribute('aria-pressed',
this.getAttribute('aria-pressed') === 'true' ? 'false' : 'true')"
>
Bold
</button>
<!-- ✅ Expandable section -->
<button aria-expanded="false" aria-controls="details" onclick="toggleDetails()">
Show Details
</button>
<div id="details" hidden>Details content...</div>
<!-- ✅ Current page in navigation -->
<nav aria-label="Main">
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/products" aria-current="page">Products</a>
</nav>Form Elements
<form>
<!-- Text input with explicit label -->
<label for="name">Full Name:</label>
<input type="text" id="name" name="name" required>
<!-- Input with additional description -->
<label for="email">Email Address:</label>
<input
type="email"
id="email"
name="email"
aria-describedby="email-hint"
required
>
<div id="email-hint">We'll never share your email.</div>
<!-- Radio buttons (grouped) -->
<fieldset>
<legend>Choose your plan:</legend>
<label><input type="radio" name="plan" value="basic"> Basic</label>
<label><input type="radio" name="plan" value="pro"> Pro</label>
</fieldset>
<!-- Checkbox -->
<label>
<input type="checkbox" name="newsletter" value="yes">
Subscribe to newsletter
</label>
<!-- Select dropdown -->
<label for="country">Country:</label>
<select id="country" name="country" required>
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
</select>
<button type="submit">Create Account</button>
</form>7. ARIA — When and How
ARIA (Accessible Rich Internet Applications) fills gaps in HTML semantics — but it must be used correctly, since incorrect ARIA is worse than no ARIA at all.
The Five Rules of ARIA
- If a native HTML element or attribute already has the semantics and behavior you need, use it instead of repurposing an element with ARIA.
- Do not change native semantics, unless you really have to.
- All interactive ARIA controls must be usable with the keyboard.
- Do not use
role="presentation"oraria-hidden="true"on a focusable element. - All interactive elements must have an accessible name.
Common ARIA Roles
Roles are added via the role attribute (e.g. <div role="button">) and give meaning to non-semantic elements in the accessibility tree. Many HTML elements already carry an implicit role (a <button> is already role="button").
- Document Structure roles — heading, paragraph, term, definition (prefer semantic HTML for these)
- Widget roles — button, checkbox, combobox, textbox, slider
- Landmark roles — navigation, main, complementary, banner, contentinfo (use sparingly)
- Live Region roles — alert, status
- Window roles — dialog, alertdialog
- Abstract roles — structure, range, widget (not for direct use — they define the ARIA role hierarchy)
ARIA States and Properties
aria-label— provides an accessible namearia-labelledby— references element(s) that label this elementaria-describedby— references element(s) that describe this elementaria-hidden— hides an element from assistive technologiesaria-expanded— indicates expanded or collapsed statearia-pressed— indicates the pressed state of a toggle buttonaria-disabled— indicates the element is disabledaria-invalid— indicates a validation erroraria-live— indicates a region will update dynamicallyaria-current— indicates the current item in a set
ARIA Live Regions
Live regions announce dynamic content changes: aria-live="polite" announces when the user is idle, aria-live="assertive" interrupts to announce immediately, aria-atomic announces the entire region rather than just the change, role="status" is implicitly polite, and role="alert" is implicitly assertive.
<!-- ✅ Status message -->
<div role="status" aria-live="polite">
Changes saved successfully
</div>
<!-- ✅ Alert message -->
<div role="alert">
Error: Your session has expired
</div>
<!-- ✅ Loading state -->
<div aria-live="polite" aria-busy="true" aria-label="Loading content">
Loading...
</div>
<!-- ✅ Search results counter -->
<div role="status" aria-live="polite" aria-atomic="true">
Found <span id="result-count">0</span> results
</div>8. Keyboard Navigation
Keyboard accessibility ensures users with motor disabilities, blind users, and power users can fully operate a site without a mouse. Good keyboard UX rests on three pillars: everything must be focusable, focus must be visible, and focus must move in a logical, predictable order.
Focus Management
- All interactive elements must be keyboard focusable
- The focus indicator must be visible with at least 3:1 contrast
- Focus order should follow the visual layout
- Never “trap” focus — except inside modals
- When content changes (e.g. opening a dialog), move focus intentionally to the new element
Tab Order and tabindex
tabindex="0"— makes an element focusable in natural tab ordertabindex="-1"— makes an element programmatically focusable onlytabindex="1+"— creates an explicit tab order (avoid unless necessary)
Prefer native interactive elements (they carry built-in tab order), only use positive tabindex values when absolutely necessary, test tab order by hand, and implement skip links for long navigation menus.
Keyboard Event Handling
- Tab / Shift+Tab — navigate between elements
- Enter / Space — activate buttons and links
- Arrow keys — navigate within components (menus, tabs, radio groups)
- Escape — close dialogs and menus
- Home / End — jump to the first/last item in a list
Modal Dialogs and Focus Traps
A modal dialog must trap focus inside itself, return focus to the trigger element when closed, and prevent background content from receiving focus.
9. Forms and Input Validation
Form Labels and Association
Every <input> needs a properly associated <label> using for + id — visual proximity alone is not enough for accessibility.
Error Handling and Validation
Error messages must be clear (describe exactly what’s wrong), specific (not “Invalid input”), announced via aria-live, and paired with aria-invalid="true" on the field.
<label for="email">Email Address</label>
<input
id="email"
type="email"
aria-invalid="true"
aria-describedby="email-error"
/>
<p id="email-error" role="alert" aria-live="assertive">
Please enter a valid email address.
</p>Required Fields and Instructions
<form aria-labelledby="form-title" aria-describedby="form-instructions">
<h2 id="form-title">Create Account</h2>
<p id="form-instructions">
Fields marked with <span aria-label="required">*</span> are required.
</p>
<!-- Required field -->
<label for="name">
Full Name <span aria-label="required">*</span>
</label>
<input type="text" id="name" name="name" required aria-required="true">
<!-- Optional field -->
<label for="phone">
Phone Number <span class="optional">(optional)</span>
</label>
<input type="tel" id="phone" name="phone">
<button type="submit">Create Account</button>
</form>10. Color, Contrast, and Visual Design
Color Contrast Requirements
- Normal text (AA): 4.5:1
- Large text (AA): 3:1 (18pt+, or 14pt+ bold)
- Normal text (AAA): 7:1
- Large text (AAA): 4.5:1
- UI components (AA): 3:1
- Focus indicators: 3:1 against the background
Check contrast with the WebAIM Contrast Checker, Chrome DevTools’ element inspector, or the Figma/Stark plugins.
Don’t Rely on Color Alone
Never use color as the only way to convey information — users with color blindness need alternative indicators (icons, text labels, patterns).
Responsive and Zoom-Friendly Design
Users must be able to zoom to 200% without losing functionality or content: use relative units (rem, em, %) instead of fixed pixels, avoid horizontal scrolling at 200% zoom, use responsive design techniques, and test at multiple zoom levels.
Part 3 — React Native Accessibility
Mobile accessibility has unique challenges and opportunities. React Native provides cross-platform accessibility APIs, but understanding platform differences — iOS VoiceOver versus Android TalkBack — is crucial for building truly accessible mobile apps.
11. React Native Accessibility Fundamentals
Core Accessibility Props
accessible— groups children into a single accessibility element (default: true for touchables)accessibilityLabel— text read by screen readers, like alt textaccessibilityHint— additional context about what happens on activationaccessibilityRole— describes the element type (button, link, header…)accessibilityState— current state (selected, disabled, checked, expanded)accessibilityValue— current value of an adjustable element (slider, progress bar)accessibilityActions— custom actions available to the user
import React from 'react';
import { TouchableOpacity, Text, View, Alert } from 'react-native';
// Basic accessible button
function AccessibleButton() {
const handleAddToCart = () => {
Alert.alert('Item added to cart!');
};
return (
<View style={{ alignItems: 'center', justifyContent: 'center', flex: 1 }}>
<TouchableOpacity
accessible={true}
accessibilityLabel="Add to cart"
accessibilityHint="Adds this item to your shopping cart"
accessibilityRole="button"
onPress={handleAddToCart}
style={{
backgroundColor: '#007AFF',
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 8,
}}
>
<Text style={{ color: 'white', fontSize: 16 }}>Add to Cart</Text>
</TouchableOpacity>
</View>
);
}
export default AccessibleButton;Accessibility Roles
The accessibilityRole prop describes what an element is or does: button, link, header, text, image, imagebutton, search, checkbox, radio, switch, adjustable (sliders), tab, menu, menubar, and menuitem.
Accessibility State
{/* Checkbox */}
<TouchableOpacity
accessibilityRole="checkbox"
accessibilityState={{ checked: isChecked }}
onPress={() => setIsChecked(!isChecked)}
style={styles.checkboxContainer}
>
<View style={[styles.checkbox, isChecked && styles.checkedBox]}>
{isChecked && <Text style={styles.checkmark}>✓</Text>}
</View>
<Text style={styles.checkboxLabel}>Accept terms and conditions</Text>
</TouchableOpacity>Accessibility Value
import React, { useState } from 'react';
import { View, Text, Button, AccessibilityInfo } from 'react-native';
export default function AccessibilityValueExample() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(prev => prev + 1);
// Optional: Announce change to screen readers
AccessibilityInfo.announceForAccessibility(`Counter value is ${count + 1}`);
};
return (
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
}}
accessible={true}
accessibilityLabel="Counter example"
accessibilityHint="Double tap to increase the counter"
accessibilityRole="adjustable"
accessibilityValue={{ min: 0, max: 10, now: count, text: `${count}` }}
>
<Text style={{ fontSize: 24, marginBottom: 10 }}>Count: {count}</Text>
<Button title="Increase" onPress={increment} />
</View>
);
}12. Screen Readers on Mobile
VoiceOver on iOS
Enable via Settings → Accessibility → VoiceOver, a triple-click of the home/side button, or by asking Siri. Gestures: swipe right/left to move between elements, double-tap to activate, triple-tap to double-click, two-finger tap to pause/resume speech, two-finger swipe up/down to read from the top or the current position, rotate two fingers for the rotor, and two-finger double-tap for “Magic Tap” (the main action). The rotor lets users navigate by headings, links, form controls, buttons, text fields, adjustable elements, images, and containers.
TalkBack on Android
Enable via Settings → Accessibility → TalkBack, holding Volume Up + Volume Down for 3 seconds, or asking Google Assistant. Gestures: swipe right/left to move between elements, double-tap to activate, swipe down-then-up to read from the top, swipe up-then-down to read from the current position, swipe right-then-left to go back, and L-shaped gestures for shortcuts. Two-finger swipes scroll or change reading granularity (characters → words → lines → paragraphs → pages).
Platform Differences
While React Native exposes cross-platform APIs, VoiceOver and TalkBack behave differently: VoiceOver is more verbose by default, TalkBack more concise; role pronunciation differs (“button” vs “btn”); gesture sets differ; navigation modes differ (rotor vs. reading controls); focus order can vary slightly; and custom actions are accessed differently. Always test on both platforms to ensure a consistent experience.
13. Touch Targets and Gestures
Minimum Touch Target Sizes
WCAG 2.1 requires a minimum of 44×44 points (iOS) or 48×48 dp (Android); Apple HIG recommends 44×44 pt, Material Design recommends 48×48 dp. Best practice: use 48×48 dp minimum for every interactive element.
Using hitSlop and pressRetentionOffset
Small icons — back buttons, delete icons, close (“X”) buttons — are hard to tap reliably. hitSlop increases the touchable area around a component without changing its visual size. pressRetentionOffset defines how far a finger can drift outside the button before the press is cancelled, keeping small controls responsive even with slight finger movement.
{/* ❌ BAD BUTTON - too small, hard to tap, no accessibility */}
<View>
<Text>❌ Bad Icon Button</Text>
<TouchableOpacity>
<Image
source={{ uri: "https://img.icons8.com/ios-filled/50/000000/chevron-left.png" }}
style={{ width: 20, height: 20 }} // too small!
/>
</TouchableOpacity>
</View>
{/* ✅ GOOD BUTTON - large touch area + accessibility */}
<View>
<Text>✅ Good Icon Button</Text>
<TouchableOpacity
accessible
accessibilityRole="button"
accessibilityLabel="Go back"
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
style={{
padding: 12, // ensures minimum 48x48 touch area
backgroundColor: "#E5E7EB",
borderRadius: 8,
width: 48,
height: 48,
justifyContent: "center",
alignItems: "center",
}}
>
<Image
source={{ uri: "https://img.icons8.com/ios-filled/50/000000/chevron-left.png" }}
style={{ width: 20, height: 20 }}
/>
</TouchableOpacity>
</View>Gesture Conflicts with Screen Readers
Custom swipe, pinch/zoom, and multi-touch gestures can conflict with screen reader navigation or magnification gestures. Solution: always provide alternative controls, like buttons or menus.
14. Forms and Input in React Native
Accessible Text Inputs
Text inputs must always have clear, descriptive labels, use accessibilityLabel when a visual label isn’t programmatically connected, provide hints for expected input, announce errors and success states, and move focus to the input when needed.
Checkboxes and Radio Buttons
React Native has no built-in checkbox or radio component, so custom implementations must expose the correct roles, states, and labels — without them, screen readers can’t tell whether an option is selected. Use accessibilityRole="checkbox" or "radio", pair it with accessibilityState={{ checked }} (checkboxes) or {{ selected }} (radios), wrap each option in a 48×48 touch target, and group radio buttons in a container with accessibilityRole="radiogroup".
Form Validation and Error Messages
Display errors close to the input, give them accessibilityRole="alert" so screen readers announce them, move focus to the first invalid field, announce results like “Email required,” and never rely on color alone (e.g. a red border) to signal an error.
import React, { useState, useRef, useEffect } from "react";
import {
View, Text, TextInput, TouchableOpacity,
AccessibilityInfo, findNodeHandle
} from "react-native";
export default function AccessibleForm() {
const [name, setName] = useState("");
const [gender, setGender] = useState("");
const [terms, setTerms] = useState(false);
const [error, setError] = useState("");
const errorRef = useRef(null);
const handleSubmit = () => {
if (!name) return setError("Name is required");
if (!gender) return setError("Select a gender");
if (!terms) return setError("Accept terms to continue");
setError("");
AccessibilityInfo.announceForAccessibility("Form submitted");
};
useEffect(() => {
if (error && errorRef.current) {
const tag = findNodeHandle(errorRef.current);
AccessibilityInfo.setAccessibilityFocus(tag);
}
}, [error]);
return (
<View style={{ padding: 20, gap: 12 }}>
{/* Text Input */}
<TextInput
value={name}
onChangeText={setName}
accessibilityLabel="Name"
accessibilityHint="Enter your name"
style={{ borderWidth: 1, padding: 10 }}
/>
{/* Radio Group */}
<View accessibilityRole="radiogroup">
{["Male", "Female"].map((option) => (
<TouchableOpacity
key={option}
accessibilityRole="radio"
accessibilityState={{ selected: gender === option }}
onPress={() => setGender(option)}
>
<Text>{gender === option ? "●" : "○"} {option}</Text>
</TouchableOpacity>
))}
</View>
{/* Checkbox */}
<TouchableOpacity
accessibilityRole="checkbox"
accessibilityState={{ checked: terms }}
onPress={() => setTerms(!terms)}
>
<Text>{terms ? "☑" : "☐"} Accept terms</Text>
</TouchableOpacity>
{/* Error */}
{error && (
<Text
ref={errorRef}
accessible
accessibilityRole="alert"
style={{ color: "red" }}
>
{error}
</Text>
)}
<TouchableOpacity
accessibilityRole="button"
onPress={handleSubmit}
style={{ padding: 12, backgroundColor: "#2563EB" }}
>
<Text style={{ color: "white" }}>Submit</Text>
</TouchableOpacity>
</View>
);
}15. Focus Management and Navigation
In dynamic interfaces, content appears, updates, or moves based on user actions. Screen reader users can lose their place if focus isn’t handled deliberately — managing it programmatically tells assistive technology exactly where the user should land next.
import React, { useRef, useState, useEffect } from 'react';
import { View, Text, TextInput, Button } from 'react-native';
export default function ManageFocusExample() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
// After submission, focus on the next important element
if (submitted) {
emailRef.current?.focus();
}
}, [submitted]);
return (
<View style={{ padding: 16 }}>
<Text>Name</Text>
<TextInput
ref={nameRef}
placeholder="Enter your name"
accessibilityLabel="Name input"
style={{ borderWidth: 1, marginBottom: 10, padding: 8 }}
/>
<Text>Email</Text>
<TextInput
ref={emailRef}
placeholder="Enter your email"
accessibilityLabel="Email input"
style={{ borderWidth: 1, marginBottom: 10, padding: 8 }}
/>
<Button title="Next" onPress={() => setSubmitted(true)} />
</View>
);
}Screen Reader Announcements
When your app updates content dynamically — loading data, showing an error, submitting a form, changing a section — screen reader users won’t know unless it’s announced. Use announcements for status updates, form errors, search/filter results, background events, and navigation changes that don’t move focus automatically. Keep announcements short, clear, relevant, and triggered only when necessary to avoid noise.
import React, { useState } from 'react';
import { View, Text, Button, AccessibilityInfo } from 'react-native';
export default function ScreenReaderAnnouncement() {
const [count, setCount] = useState(0);
const increment = () => {
const newCount = count + 1;
setCount(newCount);
AccessibilityInfo.announceForAccessibility(`Count updated to ${newCount}`);
};
return (
<View style={{ padding: 16 }}>
<Text accessibilityLiveRegion="polite">Count: {count}</Text>
<Button title="Increase Count" onPress={increment} />
</View>
);
}16. Lists, Navigation, and Complex Components
Accessible FlatList and SectionList
Lists need proper announcements when items load or refresh, a clear predictable focus order, roles that let screen readers identify list items, fallback labels for images/icons inside items, and keyboard navigation support where applicable.
<FlatList
data={items}
keyExtractor={(item) => item.id}
accessibilityLabel="Product list"
accessibilityRole="list"
onEndReached={() => {
AccessibilityInfo.announceForAccessibility("Loading more items");
}}
renderItem={({ item }) => (
<View
accessible
accessibilityRole="button"
accessibilityLabel={`${item.name}, price ${item.price}`}
>
<Text>{item.name}</Text>
<Text>{item.price}</Text>
</View>
)}
/>Tab Navigation
Tab bars need a tab role per tab, state announcements (“selected” / “not selected”), clear focus movement on selection, and no decorative-only elements receiving focus.
<View accessibilityRole="tablist">
{tabs.map((tab, index) => (
<TouchableOpacity
key={tab.key}
accessibilityRole="tab"
accessibilityState={{ selected: index === activeTab }}
accessibilityLabel={tab.label}
onPress={() => setActiveTab(index)}
>
<Text>{tab.label}</Text>
</TouchableOpacity>
))}
</View>Accessible Modals and Bottom Sheets
Modals and bottom sheets should announce themselves to screen readers and temporarily take over accessibility focus. Users must not be able to navigate to elements behind them while open; every control inside must be labeled and reachable by swipe navigation; and closing the sheet should return focus to the element that opened it, so the experience stays smooth and predictable for assistive technology users.
17. Testing React Native Accessibility
Automated Testing
Integrate accessibility checks into CI/CD so they run automatically on every commit, alongside manual and screen-reader testing.
Manual Testing Checklist
iOS VoiceOver:
- Navigate the whole app using swipe gestures and verify every interactive element announces correctly
- Test full user flows (login, checkout, etc.)
- Check images have appropriate labels, and form labels/errors are announced
- Confirm modals trap focus, rotor navigation works, and dynamic content is announced
- Test both portrait and landscape orientations
Android TalkBack:
- Navigate using swipe gestures and test reading-granularity controls
- Verify custom actions work, and test across different Android versions (behavior varies)
- Check heading hierarchy and form validation announcements
Using Accessibility Inspector Tools
iOS Accessibility Inspector (built into Xcode) inspects element properties, runs an audit for common issues, simulates VoiceOver without enabling it system-wide, and simulates different text sizes. The Android Accessibility Scanner (Google Play) scans screens, suggests improvements, and checks touch target size, contrast, and labels. Also test with large text sizes, reduced motion, color-blindness simulators, and switch control/switch access.
Part 4 — Best Practices, Patterns & Examples
Essential React Native accessibility checklist
- Use the appropriate accessibilityRole for every interactive element
- Provide a descriptive accessibilityLabel for buttons and images
- Add accessibilityHint when the result of an action isn't obvious
- Use accessibilityState for dynamic states (checked, selected, disabled)
- Ensure minimum 48×48 dp touch targets
- Use accessible={true} to group related elements
- Set accessible={false} for purely decorative elements
- Manage focus when content changes (modals, navigation)
- Announce important changes with AccessibilityInfo.announceForAccessibility()
- Test with both VoiceOver and TalkBack
- Use accessibilityViewIsModal for modal dialogs
- Provide custom actions for swipeable components
- Support text scaling — avoid fixed pixel sizes
- Maintain a proper heading hierarchy
- Hide decorative images from screen readers
Decorative vs. Informative vs. Incorrect
Background images, purely stylistic icons, separator lines, and aesthetic illustrations convey no meaning and aren’t necessary for a screen reader user. When an icon sits next to its own text label, hide the icon from the accessibility tree so the screen reader announces the label once, cleanly. When an icon stands alone, give it a label directly. The most common mistake is a standalone icon with no label at all — the screen reader then says nothing useful, or just “button.”
{/* ✅ Decorative: icon hidden, text carries the meaning */}
<TouchableOpacity accessibilityRole="button" accessibilityLabel="Go back" onPress={handleBack}>
<Ionicons name="arrow-back" size={24} accessible={false} importantForAccessibility="no" />
<Text>Go Back</Text>
</TouchableOpacity>
{/* ✅ Informative: icon alone, properly labeled */}
<TouchableOpacity accessibilityRole="button" accessibilityLabel="Go back" onPress={handleBack}>
<Ionicons name="arrow-back" size={24} />
</TouchableOpacity>
{/* ❌ Incorrect: icon alone, no label — announces nothing useful */}
<TouchableOpacity onPress={handleBack}>
<Ionicons name="arrow-back" size={24} />
</TouchableOpacity>Grouping Content Into a Single Accessible Element
When a card contains an image, a title, and a subtitle, exposing every child individually can create noise, double announcements, or break the intended meaning. Treat the whole card as one accessible unit and hide its children from the accessibility tree — useful for product cards, profile cards, notification cards, or list items with an image, title, and subtitle. Set accessible={true} on the parent, and add accessibilityElementsHidden={true} with importantForAccessibility="no-hide-descendants" on the inner wrapper. The screen reader then announces only the parent’s accessibilityLabel and accessibilityHint, instead of a clutter of separately focusable items.
<TouchableOpacity
accessible={true}
accessibilityRole="button"
accessibilityLabel={`${title}, ${subtitle}`}
onPress={onPress}
style={styles.card}
>
{/* Hidden internal elements (treated as one group) */}
<View
accessibilityElementsHidden={true}
importantForAccessibility="no-hide-descendants"
style={{ flexDirection: "row", alignItems: "center" }}
>
<Image source={{ uri: imageUri }} style={styles.avatar} />
<View style={{ marginLeft: 15 }}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
</View>
</View>
</TouchableOpacity>Touch Areas and Color Contrast
Interactive elements need comfortable touch targets with real separation between controls — tight spacing hurts users with motor impairments, larger fingers, or small screens. Pair that with strong color contrast: WCAG requires at least 4.5:1 for normal text, 3:1 for large or bold text, and 3:1 for interactive components like buttons or icons against their background. Poor contrast blurs visual boundaries, increases eye strain, and creates real barriers for users with low vision or color-perception differences — choosing strong, accessible color pairs keeps interactions confident and inclusive for everyone.
Frequently Asked Questions
- What are the WCAG POUR principles?
- POUR stands for Perceivable, Operable, Understandable, and Robust — the four principles WCAG 2.1/2.2 are built on. Perceivable means users can perceive the content (text alternatives, captions, contrast). Operable means the interface can be used, including by keyboard alone. Understandable means content and interactions behave predictably. Robust means the code works reliably across current and future assistive technologies.
- What color contrast ratio does WCAG AA require?
- WCAG 2.1 Level AA requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text (18pt+, or 14pt+ bold). UI components and focus indicators need at least 3:1 contrast against their background. Level AAA raises this to 7:1 for normal text.
- When should I use ARIA instead of semantic HTML?
- Only when there's no native HTML element or attribute that already provides the semantics and behavior you need. The first rule of ARIA is to prefer native HTML — a <button> already has the right role, keyboard support, and focus behavior for free. Incorrect ARIA is worse than no ARIA at all.
- What is the minimum touch target size for mobile accessibility?
- WCAG 2.1 requires at least 44×44 points on iOS or 48×48 dp on Android for any interactive element. Best practice is to use 48×48 dp as the minimum across both platforms, using props like hitSlop in React Native to enlarge the touchable area without changing the visual size of an icon.
- What's the difference between VoiceOver and TalkBack?
- VoiceOver (iOS) and TalkBack (Android) are the built-in mobile screen readers. They share similar gesture patterns — swipe to move between elements, double-tap to activate — but differ in verbosity (VoiceOver is more verbose by default), role pronunciation, navigation modes (VoiceOver's rotor vs. TalkBack's reading controls), and how custom actions are exposed. Always test with both.
- How do you make a React Native app accessible?
- Use the core accessibility props on every interactive element: accessibilityRole to describe what it is, accessibilityLabel for the text a screen reader announces, accessibilityHint for extra context, and accessibilityState for dynamic states like checked or selected. Manage focus explicitly when content changes, announce dynamic updates with AccessibilityInfo.announceForAccessibility(), and test with both VoiceOver and TalkBack.
- What does aria-live do?
- aria-live tells assistive technology that a region of the page will update dynamically. aria-live="polite" announces the change when the user is idle; aria-live="assertive" interrupts immediately for urgent messages. role="status" is implicitly polite and role="alert" is implicitly assertive.
- Why does focus management matter for accessibility?
- Screen reader and keyboard users navigate linearly through focus order. If focus isn't moved deliberately when content changes — like opening a modal or submitting a form — users lose their place and can't find the new content. Proper focus management moves focus intentionally to what changed and returns it to a sensible location afterward.
Part 6 — Resources & Closing Remarks
Resources for continued learning:
- React Native Docs — Accessibility
- Apple Accessibility
- Android Accessibility
- WCAG Mobile Accessibility Mapping
- The A11y Project
Thank you
Want the offline version?
Download the full handbook as a PDF, complete with cover design and diagrams.
Download Accessibility Handbook (PDF)