TouchableRipple
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
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.
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.
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.
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).
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.
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> ); }
| Prop | Type | Default | Description |
|---|---|---|---|
onPress | (((event: GestureResponderEvent) => void) & ((e: GestureResponderEvent) => void)) | undefined | — | Function to execute on press. If not set, will cause the touchable to be disabled. |
onLongPress | (((event: GestureResponderEvent) => void) & ((e: GestureResponderEvent) => void)) | undefined | — | Function 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 | undefined | — | Content of the `TouchableRipple`. |
disabled | boolean | undefined | — | Whether 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 | — | — |
borderless | boolean | undefined | — | Whether to render the ripple outside the view bounds. |
background | Object | undefined | — | Type of background drawabale to display the feedback (Android). https://reactnative.dev/docs/touchablenativefeedback#background |
centered | boolean | undefined | — | Whether to start the ripple at the center (Web). |
rippleColor | string | undefined | — | Color of the ripple effect (Android >= 5.0 and Web). |
underlayColor | string | undefined | — | Color of the underlay for the highlight effect (Android < 5.0 and iOS). |
asChild | boolean | undefined | — | When `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. |