Skip to main content

TouchableRipple

Utilities

TouchableRipple

Pressable wrapper providing ripple feedback consistent with Material 3.

Usage

Wrap any interactive surface to align press visuals with the design system.

Highlights

  • Supports bordered and borderless modes
  • Integrates with StateLayer when hovered

When to use it

  • Custom components require Material ripple animations.
  • Need to match RN Paper/Material motion without extra deps.

Examples

Default

Preview (Web)
import { StyleSheet, View } from 'react-native';
import { TouchableRipple } from 'react-native-molecules/components/TouchableRipple';
import { Text } from 'react-native-molecules/components/Text';

const styles = StyleSheet.create({
container: {
width: 220,
height: 120,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
});

export default function Example() {
  return (
      <View style={{ gap: 12 }}>
          <TouchableRipple style={styles.container} onPress={() => console.log('Pressed')}>
              <Text>Touchable Ripple</Text>
          </TouchableRipple>
      </View>
  );
}

Press and hold

The ripple expands while the press is held and fades out on release. This is driven by Pressable's onPressIn / onPressOut lifecycle, so the timing mirrors the actual press state exactly.

Preview (Web)
import { StyleSheet } from 'react-native';
import { TouchableRipple } from 'react-native-molecules/components/TouchableRipple';
import { Text } from 'react-native-molecules/components/Text';

const styles = StyleSheet.create({
container: {
width: 220,
height: 120,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#6750A4',
},
});

export default function Example() {
  return (
      <TouchableRipple style={styles.container} onPress={() => {}}>
          <Text style={{ color: '#fff' }}>Hold to see ripple</Text>
      </TouchableRipple>
  );
}

Nested interactive elements

When a child element (button, link, input) handles its own press, the outer TouchableRipple won't show a ripple — it only activates when its own press gesture fires. This prevents ghost ripples from appearing when, for example, an icon button inside a list row is tapped.

Preview (Web)
import { StyleSheet, View } from 'react-native';
import { TouchableRipple } from 'react-native-molecules/components/TouchableRipple';
import { Text } from 'react-native-molecules/components/Text';
import { Button } from 'react-native-molecules/components/Button';

const styles = StyleSheet.create({
row: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
width: 300,
height: 56,
borderRadius: 8,
},
});

export default function Example() {
  return (
      <TouchableRipple style={styles.row} onPress={() => console.log('row pressed')}>
          <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
              <Text>List row</Text>
              <Button onPress={() => console.log('action pressed')}>Action</Button>
          </View>
      </TouchableRipple>
  );
}

asChild

When asChild is true, TouchableRipple won't render a wrapper element. Instead, it merges its props (styles, event handlers, ref) onto its immediate child element. This follows the Radix UI "Slot" pattern for flexible component composition.

note

The child must be a Pressable-like component for the ripple to fire — TouchableRipple drives the ripple off the press lifecycle, so a plain View child won't show ripples. To compose with non-pressable wrappers like Surface, invert the relationship: make the wrapper asChild and let TouchableRipple be the pressable child (this is how Button is built).

note

When using asChild, only a single child element is allowed. On native Android, the ripple effect won't work with asChild since TouchableNativeFeedback requires a View wrapper.

Preview (Web)
import { StyleSheet } from 'react-native';
import { TouchableRipple } from 'react-native-molecules/components/TouchableRipple';
import { Text } from 'react-native-molecules/components/Text';
import { Surface } from 'react-native-molecules/components/Surface';

const styles = StyleSheet.create({
card: {
width: 220,
height: 120,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
});

export default function Example() {
  return (
      <Surface asChild elevation={2}>
          <TouchableRipple style={styles.card} onPress={() => console.log('Pressed')}>
              <Text>Pressable Surface (asChild)</Text>
          </TouchableRipple>
      </Surface>
  );
}
PropTypeDefaultDescription
onPress(((event: GestureResponderEvent) => void) & ((e: GestureResponderEvent) => void)) | undefinedFunction to execute on press. If not set, will cause the touchable to be disabled.
onLongPress(((event: GestureResponderEvent) => void) & ((e: GestureResponderEvent) => void)) | undefinedFunction to execute on long press.
children((string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<string | number | bigint | boolean | ReactPortal | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | ((state: PressableStateCallbackType) => React.ReactNode)) & (string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<string | number | bigint | boolean | ReactPortal | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined>)) | null | undefinedContent of the `TouchableRipple`.
disabledboolean | undefinedWhether to prevent interaction with the touchable.
style((false | "" | ViewStyle | RegisteredStyle<ViewStyle> | RecursiveArray<Falsy | ViewStyle | RegisteredStyle<ViewStyle>> | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>)) & (false | "" | ViewStyle | RegisteredStyle<ViewStyle> | RecursiveArray<Falsy | ViewStyle | RegisteredStyle<ViewStyle>>)) | null | undefined
borderlessboolean | undefinedWhether to render the ripple outside the view bounds.
backgroundObject | undefinedType of background drawabale to display the feedback (Android). https://reactnative.dev/docs/touchablenativefeedback#background
centeredboolean | undefinedWhether to start the ripple at the center (Web).
rippleColorstring | undefinedColor of the ripple effect (Android >= 5.0 and Web).
underlayColorstring | undefinedColor of the underlay for the highlight effect (Android < 5.0 and iOS).
asChildboolean | undefinedWhen `true`, the component will not render a wrapper element. Instead, it will merge its props (styles, event handlers, ref) onto its immediate child element. This follows the Radix UI "Slot" pattern for flexible component composition.
Defined in react-native-molecules/components/TouchableRipple