Introduction

Accessibility is not optional. Over one billion people worldwide live with some form of disability. That represents approximately 15% of your potential users. Beyond the moral imperative, many jurisdictions now require digital accessibility by law.

Building accessible apps also benefits everyone. Large touch targets help users with motor impairments and users in moving vehicles. Good colour contrast helps users with visual impairments and users in bright sunlight. Clear, consistent navigation helps users with cognitive impairments and every user in a hurry.

This guide covers practical accessibility implementation for mobile apps. We focus on WCAG 2.1 Level AA compliance, which is the standard most regulations reference, with implementation examples for React Native and native iOS/Android.

Understanding WCAG for Mobile

The Web Content Accessibility Guidelines apply to mobile apps despite the “Web” in the name. Mobile-specific guidance is provided in WCAG 2.1 and the accompanying mobile techniques.

WCAG Principles: POUR

Perceivable: Users must be able to perceive the information presented.

  • Provide text alternatives for images
  • Ensure sufficient colour contrast
  • Make content adaptable to different presentations

Operable: Users must be able to operate the interface.

  • Make all functionality keyboard/switch accessible
  • Give users enough time
  • Avoid designs that cause seizures
  • Provide clear navigation

Understandable: Users must understand the information and interface.

  • Make text readable
  • Make behaviour predictable
  • Help users avoid and correct mistakes

Robust: Content must work with current and future assistive technologies.

  • Maximise compatibility with screen readers
  • Use standard platform components when possible

Compliance Levels

Level A: Minimum accessibility. Required for any public-facing app.

Level AA: Standard compliance. Required by most regulations including ADA, Section 508, EN 301 549.

Level AAA: Enhanced accessibility. Rarely required by law but beneficial for users with significant disabilities.

Screen Reader

Support

Screen readers are essential assistive technology for users with visual impairments. iOS uses VoiceOver; Android uses TalkBack.

React Native Accessibility Properties

// Basic accessible button
<TouchableOpacity
  accessible={true}
  accessibilityLabel="Add item to cart"
  accessibilityHint="Adds the current product to your shopping cart"
  accessibilityRole="button"
  onPress={handleAddToCart}
>
  <Text>Add to Cart</Text>
</TouchableOpacity>

// Accessible image
<Image
  source={productImage}
  accessible={true}
  accessibilityLabel="Product photo showing red running shoes from side view"
/>

// Decorative image (should be hidden from screen readers)
<Image
  source={decorativeLine}
  accessible={false}
  accessibilityElementsHidden={true}
  importantForAccessibility="no-hide-descendants"
/>

Accessibility Roles

Use the correct role to convey the purpose of elements:

// Button
<View accessibilityRole="button">

// Link
<Text accessibilityRole="link">

// Header
<Text accessibilityRole="header">

// Image
<Image accessibilityRole="image">

// Search field
<TextInput accessibilityRole="search">

// Checkbox
<View
  accessibilityRole="checkbox"
  accessibilityState={{ checked: isChecked }}
>

// Switch/toggle
<View
  accessibilityRole="switch"
  accessibilityState={{ checked: isEnabled }}
>

// Tab
<View
  accessibilityRole="tab"
  accessibilityState={{ selected: isActive }}
>

// Alert
<View accessibilityRole="alert">

// Progress indicator
<View
  accessibilityRole="progressbar"
  accessibilityValue={{ min: 0, max: 100, now: progress }}
>

Grouping and Focus

Control how screen readers navigate your interface:

// Group related elements
<View
  accessible={true}
  accessibilityLabel="Product: Red Running Shoes, $89.99, 4.5 stars"
>
  <Text>Red Running Shoes</Text>
  <Text>$89.99</Text>
  <StarRating rating={4.5} />
</View>

// Control focus order
<View>
  <TextInput
    accessibilityLabel="Email"
    accessibilityOrder={1}
  />
  <TextInput
    accessibilityLabel="Password"
    accessibilityOrder={2}
  />
  <TouchableOpacity accessibilityOrder={3}>
    <Text>Sign In</Text>
  </TouchableOpacity>
</View>

Live Regions for Dynamic Content

Announce changes to users:

// iOS: Use accessibilityLiveRegion
<View
  accessibilityLiveRegion="polite" // or "assertive"
  accessibilityLabel={`Cart updated. ${itemCount} items total.`}
>
  <Text>{itemCount} items</Text>
</View>

// Programmatic announcement
import { AccessibilityInfo } from 'react-native';

function handleAddToCart() {
  addItemToCart(product);

  // Announce to screen reader
  AccessibilityInfo.announceForAccessibility(
    `Added ${product.name} to cart. Cart now has ${cartCount} items.`
  );
}

iOS Native VoiceOver

// UIKit
button.accessibilityLabel = "Add item to cart"
button.accessibilityHint = "Adds the current product to your shopping cart"
button.accessibilityTraits = .button

// Grouping
containerView.isAccessibilityElement = true
containerView.accessibilityLabel = "Product: Red Running Shoes, $89.99"

// Live announcements
UIAccessibility.post(notification: .announcement, argument: "Item added to cart")

// SwiftUI
Button(action: addToCart) {
    Text("Add to Cart")
}
.accessibilityLabel("Add item to cart")
.accessibilityHint("Adds the current product to your shopping cart")

Android Native TalkBack

// View accessibility
button.contentDescription = "Add item to cart"
button.accessibilityHint = "Adds the current product to your shopping cart"

// Grouping
containerView.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
containerView.contentDescription = "Product: Red Running Shoes, $89.99"

// Live announcements
containerView.announceForAccessibility("Item added to cart")

// Jetpack Compose
Button(
    onClick = { addToCart() },
    modifier = Modifier.semantics {
        contentDescription = "Add item to cart"
    }
)

Colour and Contrast

Colour and Contrast Infographic

Minimum Contrast Ratios

WCAG AA requires:

  • 4.5:1 for normal text (under 18pt or 14pt bold)
  • 3:1 for large text (18pt+ or 14pt+ bold)
  • 3:1 for UI components and graphics

Checking Contrast

// Calculate contrast ratio
function getContrastRatio(color1: string, color2: string): number {
  const luminance1 = getRelativeLuminance(color1);
  const luminance2 = getRelativeLuminance(color2);

  const lighter = Math.max(luminance1, luminance2);
  const darker = Math.min(luminance1, luminance2);

  return (lighter + 0.05) / (darker + 0.05);
}

function getRelativeLuminance(hexColor: string): number {
  const rgb = hexToRgb(hexColor);
  const [r, g, b] = rgb.map((c) => {
    c = c / 255;
    return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
  });
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

// Usage
const ratio = getContrastRatio('#333333', '#FFFFFF'); // Returns ~12.63
const meetsAA = ratio >= 4.5;
const meetsAAA = ratio >= 7;

Accessible Colour Palette

// Design system with accessible colours
const colors = {
  // Text colours with sufficient contrast on white
  text: {
    primary: '#1A1A1A',   // 16.5:1 on white
    secondary: '#4A4A4A', // 8.5:1 on white
    disabled: '#717171',  // 4.5:1 on white (minimum)
  },

  // Interactive colours
  interactive: {
    primary: '#0052CC',   // 7.1:1 on white
    hover: '#0747A6',     // 9.6:1 on white
    focus: '#0052CC',
  },

  // Status colours
  status: {
    error: '#C9252D',     // 5.6:1 on white
    success: '#006644',   // 7.9:1 on white
    warning: '#974F0C',   // 5.0:1 on white
  },

  // Ensure text on coloured backgrounds
  onPrimary: '#FFFFFF',   // 7.1:1 on #0052CC
  onError: '#FFFFFF',     // 5.6:1 on #C9252D
};

Don’t Rely on Colour Alone

Information conveyed by colour must have an additional visual indicator:

// Bad: Colour alone indicates status
<View style={{ backgroundColor: isValid ? 'green' : 'red' }} />

// Good: Icon + colour + text
<View style={styles.statusContainer}>
  <Icon
    name={isValid ? 'check-circle' : 'error'}
    color={isValid ? colors.success : colors.error}
  />
  <Text style={isValid ? styles.successText : styles.errorText}>
    {isValid ? 'Valid input' : 'Please correct this field'}
  </Text>
</View>

Touch Targets

Minimum Size Requirements

WCAG 2.1 Success Criterion 2.5.5 requires:

  • 44x44 CSS pixels minimum for touch targets

Apple Human Interface Guidelines recommend:

  • 44x44 points minimum

Material Design recommends:

  • 48x48 dp minimum
// Ensure minimum touch target size
const styles = StyleSheet.create({
  touchableButton: {
    minHeight: 48,
    minWidth: 48,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 12,
  },
  iconButton: {
    width: 48,
    height: 48,
    justifyContent: 'center',
    alignItems: 'center',
  },
  // For small visual elements, expand the touch area
  smallIconWrapper: {
    padding: 12, // Adds to the total touchable area
  },
});

function SmallIconButton({ icon, onPress, accessibilityLabel }) {
  return (
    <TouchableOpacity
      onPress={onPress}
      style={styles.smallIconWrapper}
      accessible={true}
      accessibilityLabel={accessibilityLabel}
      accessibilityRole="button"
    >
      <Icon name={icon} size={24} />
    </TouchableOpacity>
  );
}

Spacing Between Targets

Ensure adequate spacing to prevent accidental activation:

const styles = StyleSheet.create({
  buttonContainer: {
    flexDirection: 'row',
    gap: 8, // Minimum 8dp between adjacent targets
  },
  listItem: {
    padding: 16,
    marginBottom: 8, // Separation between items
  },
});

Form Accessibility

Labels and Instructions

function AccessibleForm() {
  const [email, setEmail] = useState('');
  const [emailError, setEmailError] = useState('');

  return (
    <View>
      {/* Visible label */}
      <Text
        nativeID="email-label"
        style={styles.label}
      >
        Email address
      </Text>

      {/* Helper text */}
      <Text
        nativeID="email-hint"
        style={styles.helperText}
      >
        We'll send your receipt to this address
      </Text>

      {/* Input with proper accessibility */}
      <TextInput
        value={email}
        onChangeText={setEmail}
        accessibilityLabel="Email address"
        accessibilityHint="We'll send your receipt to this address"
        accessibilityLabelledBy="email-label"
        accessibilityDescribedBy={emailError ? 'email-error' : 'email-hint'}
        accessibilityState={{
          disabled: false,
          error: !!emailError
        }}
        keyboardType="email-address"
        autoComplete="email"
        autoCapitalize="none"
        style={[
          styles.input,
          emailError ? styles.inputError : null
        ]}
      />

      {/* Error message */}
      {emailError && (
        <Text
          nativeID="email-error"
          style={styles.errorText}
          accessibilityRole="alert"
        >
          {emailError}
        </Text>
      )}
    </View>
  );
}

Error Handling

function validateAndSubmit() {
  const errors = validateForm(formData);

  if (Object.keys(errors).length > 0) {
    // Focus first error field
    const firstErrorField = Object.keys(errors)[0];
    refs[firstErrorField].current?.focus();

    // Announce errors to screen reader
    const errorSummary = Object.values(errors).join('. ');
    AccessibilityInfo.announceForAccessibility(
      `Form has ${Object.keys(errors).length} errors. ${errorSummary}`
    );

    setFormErrors(errors);
    return;
  }

  submitForm(formData);
}

Focus Management

Focus Indicators

Ensure visible focus states for keyboard/switch users:

const styles = StyleSheet.create({
  button: {
    padding: 12,
    borderRadius: 8,
    backgroundColor: colors.primary,
  },
  buttonFocused: {
    // Visible focus ring
    borderWidth: 3,
    borderColor: colors.focusRing,
    // Or use outline on web
  },
});

function AccessibleButton({ onPress, title }) {
  const [isFocused, setIsFocused] = useState(false);

  return (
    <TouchableOpacity
      onPress={onPress}
      onFocus={() => setIsFocused(true)}
      onBlur={() => setIsFocused(false)}
      style={[styles.button, isFocused && styles.buttonFocused]}
      accessible={true}
      accessibilityRole="button"
    >
      <Text style={styles.buttonText}>{title}</Text>
    </TouchableOpacity>
  );
}

Managing Focus on Navigation

function ProductScreen({ route }) {
  const headingRef = useRef<Text>(null);

  // Focus heading when screen becomes active
  useFocusEffect(
    useCallback(() => {
      // Small delay to ensure screen is rendered
      const timer = setTimeout(() => {
        headingRef.current?.focus();
        AccessibilityInfo.announceForAccessibility(
          `${route.params.productName} screen`
        );
      }, 100);

      return () => clearTimeout(timer);
    }, [route.params.productName])
  );

  return (
    <View>
      <Text
        ref={headingRef}
        accessibilityRole="header"
        accessible={true}
      >
        {route.params.productName}
      </Text>
      {/* Rest of screen */}
    </View>
  );
}
function AccessibleModal({ visible, onClose, children }) {
  const modalRef = useRef<View>(null);

  useEffect(() => {
    if (visible) {
      // Focus the modal when it opens
      AccessibilityInfo.setAccessibilityFocus(modalRef.current);
    }
  }, [visible]);

  if (!visible) return null;

  return (
    <Modal
      visible={visible}
      onRequestClose={onClose}
      transparent
    >
      <View style={styles.overlay}>
        <View
          ref={modalRef}
          style={styles.modalContent}
          accessible={true}
          accessibilityViewIsModal={true} // iOS: traps focus
          accessibilityRole="dialog"
          accessibilityLabel="Confirmation dialog"
        >
          {children}

          <TouchableOpacity
            onPress={onClose}
            accessibilityLabel="Close dialog"
            accessibilityRole="button"
          >
            <Text>Close</Text>
          </TouchableOpacity>
        </View>
      </View>
    </Modal>
  );
}

Motion and Animation

Respecting Reduced Motion

import { AccessibilityInfo, useReducedMotion } from 'react-native';

function AnimatedComponent() {
  // React Native 0.71+ hook
  const prefersReducedMotion = useReducedMotion();

  // For older versions
  const [reducedMotion, setReducedMotion] = useState(false);
  useEffect(() => {
    AccessibilityInfo.isReduceMotionEnabled().then(setReducedMotion);
    const subscription = AccessibilityInfo.addEventListener(
      'reduceMotionChanged',
      setReducedMotion
    );
    return () => subscription.remove();
  }, []);

  const animationConfig = prefersReducedMotion
    ? { duration: 0 } // No animation
    : { duration: 300, easing: Easing.bezier(0.25, 0.1, 0.25, 1) };

  return (
    <Animated.View style={animatedStyles}>
      {/* Content */}
    </Animated.View>
  );
}

Safe Animation Practices

// Avoid animations that could trigger vestibular issues
const safeAnimations = {
  // Good: Opacity fades
  fadeIn: {
    from: { opacity: 0 },
    to: { opacity: 1 },
    duration: 200,
  },

  // Good: Subtle scale
  scaleIn: {
    from: { transform: [{ scale: 0.95 }] },
    to: { transform: [{ scale: 1 }] },
    duration: 150,
  },

  // Avoid: Large parallax scrolling
  // Avoid: Zooming animations covering large areas
  // Avoid: Spinning/rotating animations
  // Avoid: Bouncing animations
};

Testing Accessibility

Manual Testing Checklist

  1. Screen Reader Testing

    • Navigate entire app with VoiceOver (iOS) and TalkBack (Android)
    • Verify all content is announced
    • Check logical reading order
    • Test all interactive elements
  2. Visual Testing

    • Enable grayscale to test colour independence
    • Test with display zoom/large text enabled
    • Verify focus indicators are visible
    • Check contrast ratios
  3. Motor Testing

    • Navigate with switch control
    • Test with external keyboard
    • Verify touch targets are adequate

Automated Testing

// Jest + Testing Library accessibility tests
import { render, screen } from '@testing-library/react-native';

describe('Accessibility', () => {
  it('button has accessible name', () => {
    render(<AddToCartButton product={mockProduct} />);

    const button = screen.getByRole('button', { name: /add to cart/i });
    expect(button).toBeTruthy();
  });

  it('image has alt text', () => {
    render(<ProductImage source={mockImage} name="Red Shoes" />);

    const image = screen.getByRole('image', { name: /red shoes/i });
    expect(image).toBeTruthy();
  });

  it('form inputs have labels', () => {
    render(<CheckoutForm />);

    expect(screen.getByLabelText(/email/i)).toBeTruthy();
    expect(screen.getByLabelText(/card number/i)).toBeTruthy();
  });

  it('error messages are announced', async () => {
    const { getByRole, getByText } = render(<LoginForm />);

    fireEvent.press(getByRole('button', { name: /sign in/i }));

    await waitFor(() => {
      const errorMessage = getByText(/email is required/i);
      expect(errorMessage.props.accessibilityRole).toBe('alert');
    });
  });
});

Axe DevTools for React Native

// Setup accessibility auditing in development
import { check } from 'react-native-a11y';

if (__DEV__) {
  check().then((issues) => {
    if (issues.length > 0) {
      console.warn('Accessibility issues found:', issues);
    }
  });
}

Compliance Documentation

Accessibility Statement

Provide users with information about your app’s accessibility:

Accessibility Statement for [App Name]

We are committed to ensuring digital accessibility for people with disabilities.
We are continually improving the user experience for everyone and applying the
relevant accessibility standards.

Conformance Status:
[App Name] is partially conformant with WCAG 2.1 level AA. Partially conformant
means that some parts of the content do not fully conform to the accessibility standard.

Known Limitations:
- Some third-party content may not be fully accessible
- [List specific known issues]

Feedback:
We welcome your feedback on the accessibility of [App Name]. Please let us know
if you encounter accessibility barriers:
- Email: [email protected]
- Phone: [Phone number]

We try to respond to feedback within 5 business days.

Assessment Approach:
[Company Name] assessed the accessibility of [App Name] by the following approaches:
- Self-evaluation
- External evaluation by [Auditor Name]

Date:
This statement was created on [Date] using the W3C Accessibility Statement Generator Tool.

VPAT (Voluntary Product Accessibility Template)

For enterprise customers, prepare a VPAT documenting compliance:

CriteriaConformance LevelRemarks
1.1.1 Non-text ContentSupportsAll images have text alternatives
1.4.3 Contrast (Minimum)SupportsAll text meets 4.5:1 ratio
2.1.1 KeyboardSupportsAll functionality keyboard accessible
4.1.2 Name, Role, ValuePartially SupportsSome custom components need improvement

Conclusion

Accessibility is a journey, not a destination. Start with the fundamentals: screen reader support, colour contrast, and touch targets. Then progressively improve as you learn from user feedback.

The most important step is testing with real assistive technology. Automated tools catch maybe 30% of issues. The rest require manual testing with VoiceOver, TalkBack, and ideally with users who rely on these technologies daily.

Building accessible apps from the start is significantly easier than retrofitting. Include accessibility in your design process, your development workflow, and your QA testing. Make it part of your definition of done.

Your accessible app will reach more users, face fewer legal risks, and simply be better for everyone who uses it.


Need help making your mobile app accessible? Our team has implemented accessibility for apps used by millions. Contact us for an accessibility audit or implementation assistance.