Mohammed Abdullah KhanDownload PDF

Accessibility Handbook · v1.0

Accessibility Handbook: A Practical Guide to Web & React Native Accessibility

Everything a developer needs to build inclusive digital products — WCAG and the POUR principles, semantic HTML and ARIA, keyboard and screen-reader support, and a deep, code-first walkthrough of React Native accessibility for iOS VoiceOver and Android TalkBack.

By Mohammed Abdullah Khan Version 1.0

Contents
  1. 1. Digital Accessibility
  2. 2. Web Accessibility
  3. 3. React Native Accessibility
  4. 4. Best Practices & Examples
  5. 5. FAQ
  6. 6. Resources & Closing

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

Accessibility also benefits people with temporary impairments (a broken arm, eye surgery) or situational limitations (bright sunlight, a noisy environment, slow internet).

1.2 The Global Impact

According to the World Health Organization (WHO):

1.3B+
people experience significant disability (16% of the global population)
253M
people are visually impaired
466M
people have disabling hearing loss
$13T
market represented by people with disabilities

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

html
<!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>&copy; 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.

html
<!-- ✅ 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.

html
<!-- ✅ Accessible icon button -->
<button aria-label="Close dialog">
  <span aria-hidden="true">&times;</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

html
<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" or aria-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 name
  • aria-labelledby — references element(s) that label this element
  • aria-describedby — references element(s) that describe this element
  • aria-hidden — hides an element from assistive technologies
  • aria-expanded — indicates expanded or collapsed state
  • aria-pressed — indicates the pressed state of a toggle button
  • aria-disabled — indicates the element is disabled
  • aria-invalid — indicates a validation error
  • aria-live — indicates a region will update dynamically
  • aria-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.

html
<!-- ✅ 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 order
  • tabindex="-1" — makes an element programmatically focusable only
  • tabindex="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.

html
<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

html
<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 text
  • accessibilityHint — additional context about what happens on activation
  • accessibilityRole — 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
tsx
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

tsx
{/* 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

tsx
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.

tsx
{/* ❌ 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.

tsx
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.

tsx
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.

tsx
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.

tsx
<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.

tsx
<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.”

tsx
{/* ✅ 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.

tsx
<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:

Thank you

I’m truly fortunate to have the access, tools, and resources that enable me to build great experiences. If I can use that privilege to make these same experiences accessible to people with diverse abilities, we move one step closer to a world that’s not only more inclusive, but genuinely equal. Thank you for taking the time to learn, improve, and contribute to a better, more accessible future for everyone.

Want the offline version?

Download the full handbook as a PDF, complete with cover design and diagrams.

Download Accessibility Handbook (PDF)