In React Native, how do the role and aria-* props relate to accessibilityRole, accessibilityLabel, accessibilityState and accessibilityValue, and which wins when both are set?
answer
- web-style names, native output
- role beats accessibilityRole
- heading, img, slider vs header, image, adjustable
- aria-label beats accessibilityLabel
- state and value merge per field
basics
~20 srole and the aria-* props are aliases with web-style names for React Native's accessibility props. role takes precedence over accessibilityRole, aria-label over accessibilityLabel, and each aria state or value prop overrides only its own field, merging with the rest.
solid answer
~40 sReact Native added web-aligned aliases for its accessibility props: `role` for `accessibilityRole`, `aria-label` for `accessibilityLabel`, `aria-labelledby` for `accessibilityLabelledBy`, `aria-disabled`/`aria-checked`/`aria-selected`/`aria-busy`/`aria-expanded` for the `accessibilityState` fields, and `aria-valuemin`/`aria-valuemax`/`aria-valuenow`/`aria-valuetext` for `accessibilityValue`. They produce the same native accessibility properties — nothing becomes a DOM attribute on iOS or Android. When both are set, `role` has precedence over `accessibilityRole` and `aria-label` over `accessibilityLabel`; state and value are merged field by field, the `aria-*` value winning for its field and the object supplying the rest. Role names follow ARIA: `heading`, `img`, `slider`, `searchbox` and `presentation` map to `header`, `image`, `adjustable`, `search` and `none`. A few React Native-specific roles, such as `togglebutton` and `imagebutton`, exist only on `accessibilityRole`. Pick one style per codebase so reviews do not have to resolve conflicts.
code
tsx · 25 linesimport { Text, View } from 'react-native';
export function SectionTitle({ title }: { title: string }) {
// role uses the ARIA name; accessibilityRole would be 'header'
return (
<View role="heading">
<Text>{title}</Text>
</View>
);
}
export function SpiceLevel({ level }: { level: 1 | 2 | 3 }) {
const names = { 1: 'Mild', 2: 'Medium', 3: 'Hot' } as const;
return (
<View
accessible
role="slider"
aria-label="Spice level"
aria-valuemin={1}
aria-valuemax={3}
aria-valuenow={level}
aria-valuetext={names[level]}
/>
);
}go deeper
Recall that role and aria-label are alternative spellings of accessibilityRole and accessibilityLabel that work on iOS and Android.
Explain precedence — role and aria-label win, state and value merge per field — and the role-name differences such as heading versus header.
Pick one prop style for the codebase, enforce it in review or lint, and hunt down silent overrides where both spellings disagree.
Decide the accessibility API style for components shared across mobile and web, weighing ARIA familiarity against mobile-only roles.
## Two spellings for one model React Native's original accessibility props all start with `accessibility`: `accessibilityRole`, `accessibilityLabel`, `accessibilityState`, `accessibilityValue`. Later releases added **aliases with web-style names** so code reads like ARIA and shares more easily with React Native for Web. On iOS and Android both spellings end up as the **same native properties** that VoiceOver and TalkBack read; an `aria-label` in React Native is not a DOM attribute, because there is no DOM. ## The alias table | Alias | Maps to | Precedence when both set | | --- | --- | --- | | `role` | `accessibilityRole` | `role` wins | | `aria-label` | `accessibilityLabel` | `aria-label` wins | | `aria-labelledby` | `accessibilityLabelledBy` (Android) | alias used | | `aria-disabled`, `aria-selected`, `aria-checked`, `aria-busy`, `aria-expanded` | fields of `accessibilityState` | each alias wins for its own field | | `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext` | fields of `accessibilityValue` | each alias wins for its own field | There is **no alias for `accessibilityHint`**. ## How the merge works `View`, `Text` and `Pressable` resolve the aliases in JavaScript before rendering the native component: 1. If `aria-label` is defined, it becomes `accessibilityLabel`, replacing any value you passed. 2. If any state alias or `accessibilityState` is present, a new state object is built field by field: `disabled: ariaDisabled ?? accessibilityState?.disabled`, and so on. 3. Value works the same way: `now: ariaValueNow ?? accessibilityValue?.now`. So this component is announced as **checked and disabled**: ```tsx <View role="checkbox" accessibilityState={{ checked: false, disabled: true }} aria-checked={true} /> ``` `aria-checked` overrides `checked`; `disabled` still comes from the object. That silent override is the main argument for choosing **one style per codebase**. ## Role names differ `role` uses **ARIA names**; `accessibilityRole` uses React Native's older names. The documented `role` values map onto native roles as follows for the ones that differ: | `role` | `accessibilityRole` equivalent | | --- | --- | | `heading` | `header` | | `img` | `image` | | `slider` | `adjustable` | | `searchbox` | `search` | | `presentation` | `none` | Some roles exist only on `accessibilityRole`, such as `togglebutton`, `imagebutton` and `keyboardkey`. And `role`'s type includes ARIA roles such as `article` or `banner` that may have no direct native equivalent, so they can change little or nothing in what VoiceOver and TalkBack say. ## Details worth knowing - **`aria-labelledby`** accepts a comma-separated list of `nativeID`s; `View` splits it into an array for `accessibilityLabelledBy`, which is Android-only. - **`aria-hidden={true}`** is resolved to the iOS and Android hiding props together; hiding is a grouping subject rather than a naming one. - **Aliases are resolved in JavaScript** by `View`, `Text` and `Pressable` before the native component renders, so a custom native component that bypasses them receives only the `accessibility*` props. ## Migrating a codebase 1. Decide the target style and record it in the component guidelines. 2. Convert shared components first, so product screens inherit the change. 3. Search for elements that set **both** spellings and remove one, checking which value was actually winning. 4. Keep `accessibilityRole` where a needed role has no `role` equivalent, such as `togglebutton`. ## Choosing a style - **Teams sharing components with web** usually standardise on `role` and `aria-*`, because the same names mean something to web developers and React Native for Web renders them as ARIA. - **Mobile-only teams** can keep the `accessibility*` names, especially where they need `togglebutton` or `imagebutton`. - **Either way, do not mix both on one element.** Precedence rules make mixed props legal but confusing. ## What neither spelling does A role or an alias changes **what is announced**, not **how the element behaves**. `role="button"` on a `View` without `onPress` is still not pressable, and `role="slider"` does not make VoiceOver's swipe-up gesture change anything until the component handles the corresponding accessibility actions. ## Interview summary Say the aliases are the same model with web names, that `role` and `aria-label` take precedence, that state and value merge per field with `aria-*` winning, and that role names differ (`heading` vs `header`, `img` vs `image`, `slider` vs `adjustable`).
- In React Native, if a Pressable has accessibilityLabel='Close' and aria-label='Dismiss dialog', what is announced?"Dismiss dialog". `Pressable`, like `View` and `Text`, resolves `aria-label` first and uses `accessibilityLabel` only when the alias is undefined.
- Why does React Native accept role='heading' when accessibilityRole uses 'header'?`role` follows ARIA naming so code reads like the web and shares with React Native for Web. On iOS and Android it is mapped to the native header role, the same one `accessibilityRole="header"` produces.
saying these in an interview costs you the question
- aria-* props create DOM attributes on iOS and Android
- accessibilityRole takes precedence over role
- aria-checked replaces the whole accessibilityState object
- role and accessibilityRole accept exactly the same names
- There is an aria alias for accessibilityHint