Skip to Content
New: AI Sign Language Avatars now in beta! View Sign Language feature ->
MobileAccessibility Rules

Accessibility Rules Reference

Every WelcomingWeb mobile SDK — React Native, iOS, Android, Flutter — evaluates the same eight rules against your app’s component tree. Rule IDs, severities and WCAG mappings are identical across platforms. The only thing that changes is the platform-native code you’d write to fix each issue.

Nodes that are hidden (display: none, opacity: 0, aria-hidden, accessibilityElementsHidden, importantForAccessibility="no", excludeFromSemantics: true or pointerEvents="none") are automatically excluded from all rules.

Rule Summary

Rule IDWCAGLevelSeverityWhat It Checks
mobile-missing-accessibility-label4.1.2ACriticalInteractive elements must have a label or visible text children.
mobile-missing-accessibility-role1.3.1AMajorCustom interactive elements should have an explicit role for screen readers.
mobile-touch-target-size2.5.8AACriticalTouch targets must be at least 48×48 dp (44×44 pt on iOS, per HIG).
mobile-image-missing-label1.1.1ACriticalImages must have a label or be marked decorative.
mobile-color-contrast1.4.3AAMajorText must have at least 4.5:1 contrast (3:1 for large text).
mobile-disabled-state-missing4.1.2AMajorVisually disabled elements must also announce disabled state.
mobile-text-input-label3.3.2ACriticalText inputs must have a persistent label. Placeholder alone is insufficient.
mobile-heading-hierarchy1.3.1AMajorHeading levels must not skip (e.g. h1 → h3).

Detailed Rule Guidance

mobile-missing-accessibility-label

WCAG 2.2 · Success Criterion 4.1.2 Name, Role, Value (Level A) · Critical

Every interactive element must have a name that screen readers can announce. A button with only an icon and no text label is invisible to VoiceOver or TalkBack users.

Evidence captured: component type, component path, and whether the element has any descendant text.

How to fix:

// Missing label <TouchableOpacity onPress={submit}> <Icon name="check" /> </TouchableOpacity> // Explicit label <TouchableOpacity onPress={submit} accessibilityLabel="Submit form" > <Icon name="check" /> </TouchableOpacity>

mobile-missing-accessibility-role

WCAG 2.2 · Success Criterion 1.3.1 Info and Relationships (Level A) · Major

Roles tell screen readers what an element does. Native widgets (Button, Switch, TextInput, etc.) already carry correct roles — this rule primarily catches custom views with tap handlers that haven’t declared themselves as buttons.

How to fix:

<TouchableOpacity onPress={openMenu} accessibilityRole="button" accessibilityLabel="Open menu" > <Text>Menu</Text> </TouchableOpacity>

Common roles: button, link, image, header, search, checkbox, radio, switch, tab, menuitem.


mobile-touch-target-size

WCAG 2.2 · Success Criterion 2.5.8 Target Size (Minimum) (Level AA) · Critical

Every touch target must meet the platform-native minimum size:

  • iOS: 44 × 44 points (Apple Human Interface Guidelines)
  • Android / React Native / Flutter: 48 × 48 density-independent pixels (Material & WCAG)

This matters most for icon-only buttons, close affordances and densely packed toolbar items.

Evidence captured: measured width and height (in dp for Android/RN/Flutter, points for iOS, including hitSlop adjustments on RN).

How to fix:

// Too small — 24×24 <TouchableOpacity style={{ width: 24, height: 24 }}> <Icon name="close" size={24} /> </TouchableOpacity> // Minimum size via padding <TouchableOpacity style={{ padding: 12 }}> <Icon name="close" size={24} /> </TouchableOpacity> // Small visual, large hit area via hitSlop <TouchableOpacity style={{ width: 24, height: 24 }} hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }} > <Icon name="close" size={24} /> </TouchableOpacity>

mobile-image-missing-label

WCAG 2.2 · Success Criterion 1.1.1 Non-text Content (Level A) · Critical

Every informative image needs an alternative text representation. Decorative images must be explicitly marked so screen readers skip them rather than announcing meaningless file names.

How to fix:

// Informative <Image source={require('./charts/q4-revenue.png')} accessibilityLabel="Q4 revenue chart showing 18 percent growth" /> // Decorative <Image source={require('./decorations/divider.png')} accessible={false} />

mobile-color-contrast

WCAG 2.2 · Success Criterion 1.4.3 Contrast (Minimum) (Level AA) · Major

Body text must have a contrast ratio of at least 4.5:1 against its background. Large text (18pt regular or 14pt bold) may go as low as 3:1.

⚠️

The rule skips when no ancestor declares a background colour. On iOS, Android and Flutter, silence is better than false positives. If your app uses a themed background without an explicit backgroundColor on the root container, set one so the rule can resolve contrast accurately.

Evidence captured: foreground colour, resolved background colour, measured ratio, and the minimum ratio required for the detected text size.


mobile-disabled-state-missing

WCAG 2.2 · Success Criterion 4.1.2 Name, Role, Value (Level A) · Major

When an element is visually disabled, the disabled state must also be announced to assistive technologies. A greyed-out button that still announces “Submit, button” to a screen reader is confusing and misleading.

How to fix:

<TouchableOpacity disabled={!formIsValid} accessibilityState={{ disabled: !formIsValid }} accessibilityRole="button" accessibilityLabel="Submit form" > <Text>Submit</Text> </TouchableOpacity>

mobile-text-input-label

WCAG 2.2 · Success Criterion 3.3.2 Labels or Instructions (Level A) · Critical

Every text input needs a persistent label. Placeholder text disappears as soon as the user starts typing, so screen-reader users who tab back into the field hear nothing.

How to fix:

// Placeholder only — not enough <TextInput placeholder="Email address" /> // Explicit accessibility label <TextInput placeholder="you@example.com" accessibilityLabel="Email address" /> // Visible label + programmatic association <View> <Text nativeID="emailLabel">Email address</Text> <TextInput placeholder="you@example.com" accessibilityLabelledBy="emailLabel" /> </View>

mobile-heading-hierarchy

WCAG 2.2 · Success Criterion 1.3.1 Info and Relationships (Level A) · Major

Screen-reader users often navigate by heading. Skipping levels (for example jumping from h1 to h3) breaks that mental model.

⚠️

The rule requires explicit heading levels to fire. On React Native and Android (Views) it uses the heading’s declared level; on Flutter it uses Semantics(header: true). Compose’s Modifier.semantics { heading() } and SwiftUI’s .accessibilityAddTraits(.isHeader) are markers without numeric levels, so screens built purely in those stacks with multiple headings skip this rule.

How to fix (React Native example):

// Skipped level <Text accessibilityRole="header" aria-level={1}>Settings</Text> <Text accessibilityRole="header" aria-level={3}>Notifications</Text> // Sequential levels <Text accessibilityRole="header" aria-level={1}>Settings</Text> <Text accessibilityRole="header" aria-level={2}>Notifications</Text>

Disabling Rules

Sometimes a rule fires on a component you can’t change (for example, a third-party library). Disable the rule globally in your SDK configuration:

import { WelcomingWeb, RULES } from '@welcomingweb/react-native-sdk'; WelcomingWeb.configure({ apiKey: 'YOUR_API_KEY', appId: 'com.yourcompany.yourapp', disabledRules: [ RULES.COLOR_CONTRAST, 'mobile-heading-hierarchy', ], });
⚠️

Disabling a rule suppresses it everywhere, not just on the offending component. Prefer fixing the issue at source, or scope the suppression with the excludeScreens option for screens you can’t control.

Rule Constants

Every SDK exports its rule IDs as constants to avoid hard-coding strings:

ConstantRule ID
MISSING_LABELmobile-missing-accessibility-label
MISSING_ROLEmobile-missing-accessibility-role
TOUCH_TARGETmobile-touch-target-size
IMAGE_MISSING_LABELmobile-image-missing-label
COLOR_CONTRASTmobile-color-contrast
DISABLED_STATEmobile-disabled-state-missing
TEXT_INPUT_LABELmobile-text-input-label
HEADING_HIERARCHYmobile-heading-hierarchy

How Scoring Works

Each scan produces a score from 0 to 100 based on the issues found and the total number of components scanned:

penalty = (critical × 10) + (major × 5) + (minor × 1) max_penalty = total_components × 10 score = max(0, 100 − (penalty / max_penalty × 100))

A screen with 45 components, 2 critical issues and 1 major issue scores 100 − (25 / 450 × 100) = 94.4.

Session scores are the average of all per-screen scores. The server independently recalculates scores from its own stored data as the source of truth in the dashboard.

The formula is identical on every platform — a scan of the same screen shape from the Android SDK and the Flutter SDK produces the same score, so you can compare builds apples-to-apples even across stacks.

Platform-Specific Notes

Color contrast requires a declared background. On iOS, Android and Flutter, the rule returns nothing when no ancestor declares a background colour — silence is better than false positives. Set an explicit backgroundColor on your root container to get accurate results.

Heading hierarchy limitations. Compose (Modifier.semantics { heading() }) and SwiftUI (.accessibilityAddTraits(.isHeader)) don’t expose numeric heading levels, so the heading-hierarchy rule doesn’t fire on screens built purely in those stacks. On React Native use aria-level; on Android Views, use android:accessibilityHeading; on Flutter wrap with Semantics(header: true).

Custom view role detection. On Android, custom View subclasses with tap handlers won’t get a role automatically — they’ll trip mobile-missing-accessibility-role. Declare the role via ViewCompat.setAccessibilityDelegate or use a framework widget.

Next Steps

Last updated on