skip to content

In React Native Web, what DOM does a Pressable with role="button" produce, and which browser events fire its onPress?

level: middleimportance: should knowfreq 34%

answer

  1. role can pick the HTML tag
  2. type="button" added automatically
  3. focusable by default: tabIndex 0
  4. onPress rides the native click
  5. Space counts only for button-ish targets

basics

~20 s

React Native Web renders a Pressable with role="button" as a real button element with type="button" and tabIndex 0. onPress fires from the browser's click event, so mouse clicks, taps and Enter or Space all trigger it.

solid answer

~40 s

`Pressable` renders a `View`, and React Native Web lets certain `role` values choose a semantic tag: `role="button"` produces `<button type="button" role="button">`. `Pressable` also sets `tabIndex` to `0` (or `-1` when `disabled`), sets `aria-disabled`, and on a real `button` adds the `disabled` attribute. Unlike native, `onPress` is not tied to the responder release: it is called from the browser's `click` event, which mice, touch and keyboard all produce. For non-native elements React Native Web emulates it on key-up: Enter always, Space only when the target is a button or has `role="button"`. On the web the style and children functions also receive `hovered` and `focused`, and `onHoverIn`/`onHoverOut` report hover. Without `role`, the same component is a focusable `div` that a screen reader does not announce as a button.

code

tsx · 25 lines
tsx
import { useState } from 'react';
import { Pressable, Text } from 'react-native';

type Props = { label: string; disabled?: boolean; onPress: () => void };

export function PrimaryButton({ label, disabled, onPress }: Props) {
  const [hovered, setHovered] = useState(false);
  return (
    <Pressable
      role="button"
      aria-label={label}
      disabled={disabled}
      onPress={onPress}
      onHoverIn={() => setHovered(true)}
      onHoverOut={() => setHovered(false)}
      testID="primary-button"
      style={({ pressed }) => ({
        padding: 12,
        opacity: pressed ? 0.6 : hovered ? 0.85 : 1,
      })}
    >
      <Text>{label}</Text>
    </Pressable>
  );
}

go deeper

for a junior

Recall that Pressable works on the web, that role="button" makes it a real button element, and that the same onPress runs for clicks and taps.

for a middle

Explain how role picks the tag, how React Native roles map to ARIA, why tabIndex 0 is set, and that onPress follows the native click event.

for a senior

Cover keyboard behaviour (Enter always, Space for button-ish targets), onPress without onPressIn, disabled semantics, and hovered state for a shared design-system button.

for a principal

Decide how a shared component library enforces semantics across platforms: required role props, lint rules, and web accessibility audits as part of the component contract.

## The scenario A design-system `Button` is written once with `Pressable` and shipped to iOS, Android and a React Native Web build. On the phone it is a native view with a press handler. In the browser it has to be something a keyboard can reach, a screen reader can announce and a mouse can hover. What the browser receives depends on the props you pass. ## Which element, which attributes `Pressable` renders a `View`, and every host component in **React Native Web** goes through a helper that can swap the tag for a semantic element based on `role`: | `role` prop | HTML element | |---|---| | none | `div` | | `button` | `button`, plus `type="button"` | | `link` (with `href`) | `a` | | `heading` or `header` | `h1`, or `h2`…`h6` from `aria-level` | | `list` / `listitem` | `ul` / `li` | | `navigation`, `main`, `form`, `article` | `nav`, `main`, `form`, `article` | | `paragraph` | `p` | So `<Pressable role="button">` becomes `<button type="button" role="button" tabindex="0">`. The `type="button"` default matters: a bare `button` inside a `form` would otherwise submit it. `Pressable` adds its own attributes on the web: - **`tabIndex`**: `0` by default and `-1` when `disabled`, so the control is a keyboard tab stop even when it renders a `div`. - **`aria-disabled`** from `disabled`; on a real `button` the `disabled` attribute is also set. - A style with `cursor: 'pointer'` and `touchAction: 'manipulation'` while enabled. - `testID` becomes `data-testid`, and `aria-label` passes straight through. ## Roles are translated to ARIA React Native's own role and `accessibilityRole` vocabulary is mapped to ARIA before it is written: - `header` becomes `heading`, `image` becomes `img`, `adjustable` becomes `slider`, `summary` becomes `region`. - `none` becomes `presentation`, which has wider browser support. - `text`, `imagebutton` and `keyboardkey` have no ARIA equivalent and are ignored. - Legacy `accessibility*` props such as `accessibilityLabel` and `accessibilityRole` are still read and written as the matching `aria-*` attribute or `role`, but the `aria-*` and `role` props are the current form. ## How onPress fires in a browser On native, `onPress` fires when the responder is released inside the element. On the web, React Native Web deliberately ties `onPress` to the **native `click` event** instead: 1. A mouse click or a touch tap produces a `click`, and `onPress` runs. 2. On a **native interactive element** (`button`, `a`, `input`), the browser itself turns Enter and Space into `click`. 3. On a **non-native element** such as a `div`, React Native Web listens for `keydown` and a matching `keyup` and calls `onPress` itself. **Enter** always counts; **Space** counts only when the target is a `button` or has `role="button"`, and for a non-native button-role element the default page scroll on Space is prevented. 4. An Alt-click does not call `onPress`, and `onLongPress` has no keyboard trigger. A consequence worth saying out loud: on the web `onPress` can fire without `onPressIn` having fired first (keyboard activation), so code that assumes the sequence `onPressIn` then `onPress` breaks. ## Interaction state on the web React Native Web passes `{ pressed, hovered, focused }` to the `style` and `children` functions of `Pressable`, although React Native's own TypeScript types declare only `pressed`. Hover is never set by touch input, and `onHoverIn` and `onHoverOut` (props React Native's `Pressable` also declares) report the transitions. Tracking hover through those two callbacks is the portable way for a shared button to get a hover state without CSS pseudo-classes. ## Disabled buttons in the browser When `disabled` is `true`, `Pressable` sets `aria-disabled`, drops the element out of the tab order with `tabIndex={-1}` and swaps its style for one with `pointerEvents: 'box-none'`. On a real `button` element React Native Web also writes the native `disabled` attribute, so the browser itself refuses clicks and focus. A `div`-based control gets only the ARIA state, which is why the `role="button"` version is the more robust of the two. ## What goes wrong without role The same component without `role` renders a `div` with `tabindex="0"`. It is focusable and Enter activates it, but a screen reader announces it as a generic group, and Space does nothing. Adding `role="button"` fixes all three at once by giving the browser the element it already knows how to treat as a button.

  • In React Native Web, why can onPress fire on a Pressable without onPressIn having fired first?
    Because React Native Web calls `onPress` from the native `click` event rather than from the responder release. A keyboard activation, or any click the browser synthesises, produces `click` without the pointer sequence that drives `onPressIn`. Code that sets state in `onPressIn` and reads it in `onPress` must not assume the order on the web.
  • In React Native Web, how does a View with role="heading" and aria-level={2} render?
    As an `h2` element with `role="heading"`. The heading role picks an `h1` when no level is given and `h{level}` when `aria-level` is set, so a React Native screen title can give the browser a real document outline for screen readers and search engines.
  • Why does React Native Web add type="button" to a button it renders?
    An HTML `button` defaults to `type="submit"`, so a React Native button rendered inside a web `form` would submit the form on every press. React Native Web sets `type="button"` whenever no type was given, keeping the press a plain action.

saying these in an interview costs you the question

  • A Pressable always renders a div, so a web build is never announced as a button.
  • onPress on the web fires from pointer release, exactly as on native.
  • The Space key activates any focused Pressable.
  • role="header" is written to the DOM unchanged.
  • A Pressable is not keyboard-focusable unless you set focusable yourself.