A screen reader (TalkBack on Android, VoiceOver on iOS) reads a Text automatically, but a field needs help: its placeholder vanishes once the user types, and a label above it is, to the platform, unrelated text. aria-labelledby (Android) links the field to its label's nativeID, accessibilityHint says what to enter, role="heading" lets users jump between headings, and aria-live="polite" announces a changed error without stealing focus:
<Text style={ui.h1} role="heading">Redeem a gift card</Text>
<Text style={ui.label} nativeID="codeLabel">Gift card code</Text>
<TextInput style={ui.input} value={code} onChangeText={setCode}
aria-labelledby="codeLabel" accessibilityHint="Six characters, printed on the card"
placeholder="ABC123" autoCapitalize="characters" maxFontSizeMultiplier={1.5} />
<Text style={ui.error} aria-live="polite">
{invalid ? `Code too short: ${code.length} of 6 characters` : ''}
</Text>Here invalid is code.length > 0 && code.length < 6. You can check what the accessibility layer receives, without TalkBack, by dumping the node tree after typing "ABC":
adb -s emulator-5554 shell uiautomator dump /data/local/tmp/ui.xml
adb -s emulator-5554 shell cat /data/local/tmp/ui.xml | tr '>' '\n' \
| grep -E '(text|hint)="[^"]' | grep -oE '(text|class|hint)="[^"]*"' | paste -d' ' - - -UI hierchary dumped to: /data/local/tmp/ui.xml text="Redeem a gift card" class="android.view.View" hint="" text="Gift card code" class="android.widget.TextView" hint="" text="ABC" class="android.widget.EditText" hint="ABC123" text="Code too short: 3 of 6 characters" class="android.widget.TextView" hint=""
The placeholder reaches the platform as the field's hint and the error is a node of its own. The label link and the hint are spoken by TalkBack but not listed, so test with the real screen reader (Settings > Accessibility) before release; Accessibility and i18n audits a whole app.