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 ID | WCAG | Level | Severity | What It Checks |
|---|---|---|---|---|
mobile-missing-accessibility-label | 4.1.2 | A | Critical | Interactive elements must have a label or visible text children. |
mobile-missing-accessibility-role | 1.3.1 | A | Major | Custom interactive elements should have an explicit role for screen readers. |
mobile-touch-target-size | 2.5.8 | AA | Critical | Touch targets must be at least 48×48 dp (44×44 pt on iOS, per HIG). |
mobile-image-missing-label | 1.1.1 | A | Critical | Images must have a label or be marked decorative. |
mobile-color-contrast | 1.4.3 | AA | Major | Text must have at least 4.5:1 contrast (3:1 for large text). |
mobile-disabled-state-missing | 4.1.2 | A | Major | Visually disabled elements must also announce disabled state. |
mobile-text-input-label | 3.3.2 | A | Critical | Text inputs must have a persistent label. Placeholder alone is insufficient. |
mobile-heading-hierarchy | 1.3.1 | A | Major | Heading 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:
React Native
// 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:
React Native
<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:
React Native
// 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:
React Native
// 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:
React Native
<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:
React Native
// 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:
React Native
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:
| Constant | Rule ID |
|---|---|
MISSING_LABEL | mobile-missing-accessibility-label |
MISSING_ROLE | mobile-missing-accessibility-role |
TOUCH_TARGET | mobile-touch-target-size |
IMAGE_MISSING_LABEL | mobile-image-missing-label |
COLOR_CONTRAST | mobile-color-contrast |
DISABLED_STATE | mobile-disabled-state-missing |
TEXT_INPUT_LABEL | mobile-text-input-label |
HEADING_HIERARCHY | mobile-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.