skip to content

In React Native, why do teams build custom buttons with Pressable instead of the core Button or the Touchable* components?

level: juniorimportance: must knowfreq 66%

answer

  1. Button's title and missing prop
  2. uppercase on one platform
  3. color: text vs background
  4. function of {pressed}
  5. Touchables: one baked-in feedback each

basics

~20 s

Core Button renders a fixed platform look from a string title and has no style prop, and each Touchable bakes in one feedback effect. Pressable exposes the pressed state and the full press lifecycle, so any design can be built on it.

solid answer

~40 s

`Button` is deliberately minimal: `title` must be a string, there is no `style` prop, Android uppercases the title, and `color` tints the text on iOS but fills the background on Android. It is itself a thin wrapper over `TouchableOpacity` or, on Android, `TouchableNativeFeedback`. The `Touchable*` components each hard-code one feedback: `TouchableOpacity` fades, `TouchableHighlight` shows an underlay and needs exactly one child, and `TouchableWithoutFeedback` clones its child. `Pressable` renders its own `View`, passes `{pressed}` to function-valued `style` and `children`, and exposes `onPressIn`, `onPressOut`, `onPress`, `onLongPress`, `hitSlop` and `pressRetentionOffset`. In React Native 0.87 the Touchables still ship, but the docs point to `Pressable` as the more extensive, future-proof API.

code

tsx · 40 lines
tsx
import {Pressable, StyleSheet, Text} from 'react-native';

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

export function CalcKey({label, disabled = false, onPress}: CalcKeyProps) {
  return (
    <Pressable
      onPress={onPress}
      disabled={disabled}
      style={({pressed}) => [
        styles.key,
        pressed && styles.keyPressed,
        disabled && styles.keyDisabled,
      ]}>
      {({pressed}) => (
        <Text style={[styles.label, pressed && styles.labelPressed]}>
          {label}
        </Text>
      )}
    </Pressable>
  );
}

const styles = StyleSheet.create({
  key: {
    flex: 1,
    aspectRatio: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#2b2b2b',
  },
  keyPressed: {backgroundColor: '#4a4a4a'},
  keyDisabled: {opacity: 0.4},
  label: {fontSize: 28, color: '#ffffff'},
  labelPressed: {color: '#ffd166'},
});

go deeper

for a junior

Recall the concrete limits of Button: string title, no style prop, uppercase on Android, color meaning text on iOS and background on Android. Then show a Pressable whose style is a function of pressed.

for a middle

Explain what each Touchable bakes in and its structural catch, such as the extra Animated.View or the cloned single child, and why Pressable's function-valued style and children remove those constraints.

for a senior

Point out that the Touchables still run on the same Pressability engine, so migration is about API shape, and plan it incrementally. Catch the disabled trap: Pressable changes behaviour and accessibility state, never the visuals.

for a principal

Frame the choice as a design-system decision: one in-house key or button primitive built on Pressable gives consistent feedback, disabled styling and touch geometry across the app instead of per-screen Touchable choices.

## Why this question comes up Almost every React Native screen has something tappable that is not a list row: a calculator key, a toolbar icon, a card. React Native ships three generations of tools for that job, and interviewers use the choice between them to check whether a candidate has built real UI or only copied a starter snippet. The three are the core **`Button`**, the **`Touchable*`** family (`TouchableOpacity`, `TouchableHighlight`, `TouchableWithoutFeedback`, and the Android-only `TouchableNativeFeedback`), and **`Pressable`**. All of them are imported from `react-native`. ## What the core Button gives you, and where it stops `Button` is intentionally minimal. The docs describe it as "a basic button component that should render nicely on any platform" with "a minimal level of customization". In React Native 0.87 its source shows exactly where the customization ends: - **`title` must be a string.** An invariant in the component throws if it is not, so you cannot put an icon or two lines of styled text inside. - **There is no `style` prop.** Padding, radius, background, font and size are fixed by the component's own stylesheet. - **The title is uppercased on Android** (`title.toUpperCase()` runs when `Platform.OS === 'android'`) and left as written on iOS. - **`color` means different things per platform:** it tints the text on iOS and fills the background on Android. - **There is no `onLongPress`, `onPressIn` or pressed-state callback** on `Button` itself. - **It is built from Touchables:** on Android it renders `TouchableNativeFeedback`, elsewhere `TouchableOpacity`. `Button` is fine for a settings screen or a prototype where the platform look is wanted. It is the wrong tool for a designed key such as a calculator's `7` or `DEL`. ## The Touchable* family Before `Pressable`, custom buttons were built from the Touchables. Each one hard-codes a single kind of feedback: | Component | Feedback | Structural catch | |---|---|---| | `TouchableOpacity` | fades the child (default `activeOpacity` 0.2) | wraps children in an `Animated.View`, which the docs warn can affect layout | | `TouchableHighlight` | darkens with an underlay colour | needs exactly one child; the child should have an opaque background | | `TouchableWithoutFeedback` | none | clones its single child, which must forward the responder props | | `TouchableNativeFeedback` | native Android state drawable | Android only; a single View child | In 0.87 these components still ship and are still exported; they are **legacy, not removed**. Each doc page opens with a tip pointing to `Pressable` as the "more extensive and future-proof" way to handle touch input. Internally they already run on the same **Pressability** state machine as `Pressable`, so the difference is the API you program against, not the press engine underneath. ## What Pressable adds `Pressable` renders its own `View` around whatever children you give it and exposes the press as data rather than a baked-in effect: 1. **`style` can be a function** of `{pressed}`, so the pressed look is ordinary styling you choose. 2. **`children` can be a function** of `{pressed}`, so a label or icon can change while held. 3. **The whole lifecycle is exposed:** `onPressIn`, `onPressOut`, `onPress`, `onLongPress` (after `delayLongPress`, default 500 ms) and `onPressMove`. 4. **Touch geometry is configurable:** `hitSlop` widens where a press may start and `pressRetentionOffset` sets how far a finger may drift before the press is lost. 5. **Any number of children** is fine, because `Pressable` does not clone a single child. It is also cheap. `Pressable` is wrapped in `memo`, and it only keeps `pressed` in state when `style` or `children` is a function; with plain values a press causes no re-render of the component. ## The disabled state is yours to draw `disabled` on `Pressable` stops it from becoming the touch responder and sets `disabled` in the accessibility state it reports. It does **not** change how the key looks. `Button` greys itself out when disabled (lighter text, and a grey background on Android), so developers moving from `Button` to `Pressable` often ship a disabled key that looks live. Read the same `disabled` flag in your own style to dim the key. ## Choosing in practice - **Designed UI** (keypads, icon buttons, cards): `Pressable`. - **A quick platform-styled action** where the default look is acceptable: `Button`. - **Existing code on `TouchableOpacity`**: it works; migrate when you next touch the component rather than as a big-bang rewrite. - **Android ripple, gesture-library buttons and minimum touch-target sizing** are separate topics with their own owners.

  • Are the Touchable* components deprecated or removed in React Native 0.87?
    Neither. `TouchableOpacity`, `TouchableHighlight`, `TouchableWithoutFeedback` and `TouchableNativeFeedback` are still exported, and internally they use the same Pressability state machine as `Pressable`. Their doc pages open with a tip pointing to `Pressable` as the more extensive, future-proof API. What 0.87 removed was the undocumented `Touchable` root export, an old mixin, not these components.
  • Why does wrapping a custom component in TouchableWithoutFeedback sometimes do nothing?
    `TouchableWithoutFeedback` renders no view of its own: it takes its single child and clones it with the responder props. If that child is a custom component that does not pass those props down to a native view, nothing that can receive touches ever gets them. `Pressable` avoids this by rendering its own `View` around the children.
  • Does a Pressable re-render its subtree on every press?
    Only when you ask it to. `Pressable` keeps `pressed` in state only when `style` or `children` is a function; with plain values, press-in and press-out cause no state update. It is also wrapped in `memo`, so stable props from the parent avoid re-renders driven from above.

saying these in an interview costs you the question

  • Button accepts a style prop for padding, radius and background.
  • Button renders the same way on iOS and Android.
  • TouchableOpacity was removed from React Native core.
  • Pressable greys itself out automatically when disabled is true.
  • Pressable, like TouchableHighlight, accepts only a single child.