skip to content

In a React Native Pressable, how does the android_ripple prop work, and why might a configured ripple never appear?

level: middleimportance: should knowfreq 28%

answer

  1. native ripple drawable on Android
  2. color, borderless, radius, foreground, alpha
  3. needs color, borderless or radius
  4. foreground draws above children
  5. iOS ignores it entirely

basics

~20 s

android_ripple draws Android's native ripple on a Pressable, configured by color, borderless, radius, foreground and alpha. It is enabled only when color, borderless or radius is set, iOS ignores it, and a covering child hides it unless foreground is true.

solid answer

~50 s

`android_ripple` takes a `RippleConfig` — `color`, `borderless`, `radius`, `foreground` and `alpha` — and turns it into a native ripple drawable on the Pressable's view; on press React Native moves the ripple's hotspot to the touch point, so it spreads from the finger. Three things make a ripple silently not appear. First, the hook only enables it when `color`, `borderless` or `radius` is set, so `android_ripple={{}}` or `{ foreground: true }` alone does nothing. Second, by default the ripple is drawn as the view's background, so a child that fills the Pressable with its own background or an image covers it; `foreground: true` draws it on top instead. Third, it is Android-only: iOS ignores it, so iOS needs its own pressed feedback through the `style` function or children function. Since 0.86 `color` may be a `PlatformColor`, which follows light and dark theme changes.

code

tsx · 24 lines
tsx
import { Image, Platform, Pressable, StyleSheet } from 'react-native';

type Props = { uri: string; onOpen: () => void };

export function PolicyThumbnail({ uri, onOpen }: Props) {
  return (
    <Pressable
      onPress={onOpen}
      android_ripple={{ color: 'rgba(0, 0, 0, 0.12)', foreground: true }}
      style={({ pressed }) => [
        styles.frame,
        Platform.OS === 'ios' && pressed ? styles.pressed : null,
      ]}
    >
      <Image source={{ uri }} style={styles.image} />
    </Pressable>
  );
}

const styles = StyleSheet.create({
  frame: { borderRadius: 8, overflow: 'hidden' },
  pressed: { opacity: 0.7 },
  image: { width: 96, height: 96 },
});

go deeper

for a junior

Recall that android_ripple on Pressable gives the Android ripple, that iOS ignores it, and that color is the field to set first.

for a middle

Explain the config fields, the enable condition on color, borderless or radius, and background versus foreground drawing when children have backgrounds.

for a senior

Build press feedback that behaves on both platforms, and diagnose invisible or overflowing ripples in dense lists and icon rows.

for a principal

Own the press-feedback rule in the design system: native ripple on Android, a pressed style on iOS, shared tokens so both stay consistent.

## What `android_ripple` is Material Design's touch feedback on Android is the **ripple**: a circle of colour that spreads from the touch point. React Native's `Pressable` exposes it through the Android-only **`android_ripple`** prop. It is not an animation written in JavaScript; React Native hands Android a native ripple drawable, and Android animates it on the UI side. The prop takes a **`RippleConfig`** object: | Field | Meaning | | --- | --- | | `color` | Ripple colour; a colour value or, since 0.86, a `PlatformColor` | | `borderless` | `true` lets the ripple extend beyond the view's bounds | | `radius` | Ripple radius | | `foreground` | `true` draws the ripple over the children instead of as the background | | `alpha` | Ripple opacity from `0.0` to `1.0`, applied on top of the colour's own alpha | ## How it works under the hood Pressable calls an internal hook that, **only on Android and only when `color`, `borderless` or `radius` is not null**, builds a native ripple description and passes it to the view as either its native background or its native foreground. During the press it: 1. moves the ripple's **hotspot** to the touch coordinates on press-in and while the finger moves, so the ripple spreads from the finger; 2. sets the native view's **pressed** state on press-in, which starts the ripple; 3. clears the pressed state on press-out. The ripple itself is drawn and animated natively, not frame by frame in JavaScript. Its **start**, however, comes from Pressable's press-in handling in JavaScript, so a JavaScript thread that is already blocked when the finger lands delays the ripple along with every other press response. ## Why a configured ripple never appears - **Nothing that enables it is set.** `android_ripple={{}}` or `android_ripple={{ foreground: true }}` passes a config, but without `color`, `borderless` or `radius` the hook returns nothing and no ripple is attached. `{ color: '#00000022' }` is the minimal working config. - **A child covers it.** By default the ripple is the Pressable's **background**. If the only child is an `Image` or a `View` with its own `backgroundColor` that fills the Pressable, the ripple is painted underneath it. `foreground: true` moves the ripple above the children. - **It is iOS.** On iOS the prop is ignored. Pressed feedback there comes from Pressable's `style` function (`({ pressed }) => ...`) or its children function. - **Borderless surprises.** A `borderless` ripple is not clipped to the view, so it can spread over neighbours, which looks like a bug on a small icon button inside a row. ## Ripple versus a pressed style | | `android_ripple` | `style={({ pressed }) => ...}` | | --- | --- | --- | | Platforms | Android only | iOS and Android | | Drawn by | Android's native ripple drawable | React Native re-rendering the style | | Spreads from the touch point | Yes, via the hotspot | No, the whole view changes | You can combine both on Android — a ripple plus a subtle pressed style — but using only the ripple on Android and only the style on iOS keeps each platform idiomatic. ## Cross-platform press feedback A robust pattern combines both mechanisms: ```tsx <Pressable android_ripple={{ color: 'rgba(0, 0, 0, 0.12)', foreground: true }} style={({ pressed }) => [ styles.row, Platform.OS === 'ios' && pressed ? styles.rowPressed : null, ]} onPress={openPolicy} > <Image source={thumbnail} style={styles.thumb} /> </Pressable> ``` On Android the foreground ripple sits above the image; on iOS the pressed style dims the row. ## Related Android-only props - **`android_disableSound`** — `false` by default; set `true` to suppress the system click sound on press. - **`TouchableNativeFeedback`** — the older Android-only wrapper that also draws native feedback. React Native's docs point to `Pressable` as the more extensive and future-proof API, and new code should use `android_ripple` on `Pressable`. ## Interview summary Describe the native ripple and its config fields, then the three silent failures: the enabling fields, background versus foreground under a covering child, and iOS ignoring it. Mention that the ripple animates natively but is started from Pressable's JavaScript press-in handling.

  • In a React Native Pressable, why does android_ripple={{ foreground: true }} show no ripple at all?
    Pressable only builds the native ripple when `color`, `borderless` or `radius` is set. `foreground` alone just chooses where the ripple would be drawn, so nothing is attached. Adding a `color` is enough to enable it.
  • What changed for android_ripple colours in React Native 0.86?
    The `color` field accepts a `PlatformColor`, so a ripple can reference an Android theme attribute such as `?attr/colorAccent`. The ripple then updates automatically when the system configuration changes, for example when the user switches between light and dark mode.

saying these in an interview costs you the question

  • android_ripple also animates the press on iOS
  • An empty android_ripple object enables the default ripple
  • The ripple is animated frame by frame in JavaScript
  • foreground: true is needed for every ripple to appear
  • TouchableNativeFeedback is the recommended way to get a ripple in new code