In React Native, what is the difference between accessibilityState and accessibilityValue, and how does a disabled Pressable get announced as disabled?
answer
- state: flags about the element
- disabled, selected, checked, busy, expanded
- checked can be 'mixed'
- value: min, max, now or text
- Pressable disabled fills state
basics
~20 saccessibilityState carries flags — disabled, selected, checked (true, false or 'mixed'), busy, expanded — while accessibilityValue carries a range (min, max, now) or a text value. Pressable copies its disabled prop into accessibilityState.disabled, so it is announced as disabled.
solid answer
~50 s`accessibilityState` is an object of **flags** describing the element's condition: `disabled`, `selected`, `checked` (a boolean or `'mixed'` for a partial checkbox), `busy` and `expanded`. `accessibilityValue` describes **what the element currently holds**: for ranges, integer `min`, `max` and `now` (`min` and `max` are required once `now` is set), or a `text` description that overrides the numbers. A checkbox uses state; a slider or progress bar uses value. `Pressable` merges its own `disabled` prop into `accessibilityState.disabled`, so `<Pressable disabled>` is announced as disabled with no extra work. A "disabled" button built by ignoring presses in `onPress` is not: visually greyed out, it still sounds active. Each field also has an `aria-*` alias — `aria-disabled`, `aria-checked`, `aria-valuenow` and so on — which wins over the matching field when both are set. Both props must be updated on every render that changes the underlying state.
code
tsx · 16 linesimport { Pressable, Text } from 'react-native';
type Props = { canSubmit: boolean; submitting: boolean; onSubmit: () => void };
export function PostReviewButton({ canSubmit, submitting, onSubmit }: Props) {
return (
<Pressable
role="button"
onPress={onSubmit}
disabled={!canSubmit || submitting} // also sets accessibilityState.disabled
aria-busy={submitting}
>
<Text>{submitting ? 'Posting...' : 'Post review'}</Text>
</Pressable>
);
}go deeper
Recall that accessibilityState holds flags like disabled and checked, and accessibilityValue holds a range or a text value.
Explain the fields and types, the 'mixed' checked value, text overriding the range, and how Pressable's disabled prop reaches the screen reader.
Audit custom controls for state that drifts from visuals, hand-rolled disabled buttons and stale busy flags, and fix them at the component level.
Make state and value part of every interactive component's contract in the design system, so product teams cannot ship controls that look and sound different.
## Two different questions Assistive technology asks two separate things about a control: - **What condition is it in?** Disabled, selected, checked, busy, expanded. That is **`accessibilityState`**. - **What does it currently hold?** 3 out of 5, 40%, "Medium spicy". That is **`accessibilityValue`**. Mixing them up produces announcements that are either wrong or missing. ## `accessibilityState` | Field | Type | Typical control | | --- | --- | --- | | `disabled` | boolean | Any control that cannot be used right now | | `selected` | boolean | Tabs, chips, segmented choices | | `checked` | boolean or `'mixed'` | Checkboxes, switches, toggle buttons | | `busy` | boolean | A region that is loading | | `expanded` | boolean | Accordions, dropdowns | The `'mixed'` value represents a partially checked checkbox, such as "select all" when only some reviews are selected. Pair `checked` with a role that has a checked concept — `checkbox`, `switch`, `radio`, or `togglebutton` on `accessibilityRole` — so the reader knows how to phrase it. ## `accessibilityValue` | Field | Type | Notes | | --- | --- | --- | | `min` | integer | Required if `now` is set | | `max` | integer | Required if `now` is set | | `now` | integer | Current position in the range | | `text` | string | Human description; overrides `min`, `now` and `max` | Use numbers for true ranges — a volume slider, a download progress bar. Use `text` when the numbers alone read badly, such as `{ text: '3 of 5 stars' }` or `{ text: 'Medium' }` for a spice-level picker. ## How `disabled` reaches the screen reader `Pressable` computes the state it passes down from three sources, in this order: 1. `aria-busy`, `aria-checked`, `aria-disabled`, `aria-expanded`, `aria-selected`, each overriding the matching field; 2. the `accessibilityState` object you passed; 3. its own **`disabled` prop**, which is merged in last as `accessibilityState.disabled`. So `<Pressable disabled={!canSubmit}>` both ignores presses and is **announced as disabled**. The common bug is a hand-rolled disabled button: ```tsx <Pressable onPress={() => { if (canSubmit) submit(); }} style={canSubmit ? styles.on : styles.off}> <Text>Post review</Text> </Pressable> ``` It looks grey, but nothing tells VoiceOver or TalkBack that it is disabled, so users double-tap and nothing happens. Pass `disabled` (or `aria-disabled`) instead. ## What Android does with the state On Android, React Native copies the state onto the view's accessibility node: `selected` sets the node's selected flag, `disabled` marks it **not enabled**, and a boolean `checked` makes the node **checkable** and sets whether it is checked. TalkBack then phrases the announcement from those flags. iOS VoiceOver receives the equivalent traits and values through React Native's iOS view. The phrasing differs per platform, which is why a control should be checked with both readers. ## A `disabled` pitfall `Pressable` applies its own `disabled` prop **after** the `aria-*` aliases and `accessibilityState`, and it does so whenever `disabled` is not `null` — including `false`. So `<Pressable disabled={false} aria-disabled>` is announced as **enabled**: the explicit `disabled={false}` replaces the alias. Drive both from the same boolean, or use only one of them. ## Keeping them in sync - Derive state and value **from the same React state** that drives the visuals, in the same render. - Do not leave `accessibilityState` stale after an async change; `busy: true` must be cleared when loading ends. - Omit fields that do not apply rather than setting them all to `false`: on Android any boolean `checked`, even `false`, marks the element as checkable, so TalkBack can describe a plain button as "not checked". ## Aliases Every field has an `aria-*` alias: `aria-disabled`, `aria-selected`, `aria-checked`, `aria-busy`, `aria-expanded` for state, and `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext` for value. React Native merges them field by field, and the `aria-*` value wins when both are present. ## Interview summary State is flags, value is content; `checked` accepts `'mixed'`; value takes integer `min`/`max`/`now` or an overriding `text`; and `Pressable`'s `disabled` prop is what makes a disabled button sound disabled.
- In React Native's accessibilityValue, what happens if you set text as well as min, max and now?`text` overrides the numeric fields, so the screen reader speaks the text description. That is useful when the numbers are technically right but read badly, for example speaking "3 of 5 stars" instead of a raw number within a range.
- How do you represent a partially selected 'select all' checkbox in React Native?Give it a checkbox role and set `accessibilityState={{ checked: 'mixed' }}` or `aria-checked="mixed"`. The `'mixed'` value is how React Native represents the indeterminate state to VoiceOver and TalkBack.
State is the sign hanging on a shop door — open, closed, busy — while value is the number on the queue display inside; a greyed-out button without disabled state is a shop with the lights off but the sign still saying open.
saying these in an interview costs you the question
- accessibilityValue is where you put disabled and selected
- Greying out a button is enough for screen readers to know it is disabled
- checked only accepts true or false
- accessibilityValue.now can be set without min and max
- accessibilityState wins over aria-disabled when both are set