New in the merged package · iOS only.
When an action changes a control's value, iOS VoiceOver reads the element's
accessibilityValue / state the instant the action fires — before React re-renders.
Without help, it announces the previous value. The optimistic prop feeds the
predicted next value so VoiceOver speaks the right thing immediately, for the brief
window between the action and the next props update.
Note
This is iOS-only. On Android the prop is a no-op — the controls still work, nothing is announced optimistically.
Consider a checkbox whose accessibilityState.checked is driven by React state. When the
user double-taps:
- VoiceOver triggers the activation and immediately reads the current
accessibilityState— stillunchecked. - Your
onPressupdates state; React re-renders tocheckeda frame (or several) later.
The result: VoiceOver says "unchecked" even though the user just checked it. A simulated loading delay makes the gap wider and the stale read obvious.
Pass optimistic with the value you expect after the action. VoiceOver announces that
prediction right away.
import { A11y } from 'react-native-a11y';
const [checked, setChecked] = useState(false);
<A11y.Pressable
accessibilityRole="checkbox"
accessibilityLabel="Email notifications"
accessibilityState={{ checked }}
optimistic={{ state: !checked }} // predicted next checked value
onPress={() => setChecked((c) => !c)}
>
{/* …checkbox UI… */}
</A11y.Pressable>Each field is a static snapshot taken at action time. Omitting the whole object disables the behavior.
| Field | Type | Announced when |
|---|---|---|
state |
boolean |
On activate (double-tap) for toggles. Announced role-aware — checkbox/radio → "checked"/"unchecked", switch → "on"/"off". |
activate |
string |
On activate (double-tap). An explicit string; takes precedence over state. |
increase |
string |
After VoiceOver triggers increment (swipe up on an adjustable). |
decrease |
string |
After VoiceOver triggers decrement (swipe down on an adjustable). |
type A11yOptimisticConfig = {
increase?: string;
decrease?: string;
activate?: string;
state?: boolean;
};<A11y.Pressable
accessibilityRole="switch"
accessibilityLabel="Wi-Fi"
accessibilityState={{ checked: enabled }}
optimistic={{ state: !enabled }} // announced "on" / "off" for switch role
onPress={() => setEnabled((e) => !e)}
>
{/* …switch UI… */}
</A11y.Pressable>const next = (priority + 1) % PRIORITIES.length;
<A11y.Pressable
accessibilityRole="button"
accessibilityLabel="Priority"
accessibilityValue={{ text: PRIORITIES[priority] }}
optimistic={{ activate: PRIORITIES[next] }}
onPress={() => setPriority(next)}
>
{/* …value pill… */}
</A11y.Pressable><A11y.Pressable
accessibilityRole="adjustable"
accessibilityLabel="Quantity"
accessibilityValue={{ text: String(quantity) }}
accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
onAccessibilityAction={handleAdjust}
optimistic={{
increase: String(quantity + 1),
decrease: String(Math.max(0, quantity - 1)),
}}
>
{/* …quantity UI… */}
</A11y.Pressable>Important
increase / decrease only change what is announced — the element must still
carry the Adjustable trait (accessibilityRole="adjustable") and real
accessibilityActions to actually step the value.
The optimistic value follows the same targeting as screenReaderFocusTarget /
focusableWrapper:
- On a plain
A11y.Viewinselfmode, the prediction applies to the view itself. - On
A11y.PressableandA11y.Input, which wrap their inner component in a focusableA11y.View, the config is applied in wrapper mode — the inner focused element looks up itsA11y.Viewhost for the optimistic value. This is why the examples above putoptimisticdirectly onA11y.Pressable.
- Component overview → common focus props
- Announcements — for one-off messages not tied to a control's value
