Do not use unless you have a very good reason. All elements that respond to press should have visual feedback when touched. This is one of the primary reasons a “web” app doesn’t feel “native”.
TouchableWithoutFeedback handles touch interactions but provides no visual feedback to the user. It should only be used in specific cases where you’re implementing custom visual feedback.
Usage
import { TouchableWithoutFeedback, View, Text, StyleSheet } from 'react-native';
function App() {
return (
<TouchableWithoutFeedback onPress={() => console.log('Pressed')}>
<View style={styles.container}>
<Text>Touch me (no visual feedback)</Text>
</View>
</TouchableWithoutFeedback>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
backgroundColor: '#f0f0f0',
},
});
TouchableWithoutFeedback must have exactly one child. Use a View to wrap multiple children.
Props
onPress
(event: GestureResponderEvent) => void
Called when the touch is released, but not if cancelled (e.g., by a scroll that steals the responder lock).
onPressIn
(event: GestureResponderEvent) => void
Called when a touch is engaged, immediately when the finger touches down.
onPressOut
(event: GestureResponderEvent) => void
Called when the touch is released or cancelled.
onLongPress
(event: GestureResponderEvent) => void
Called when the user holds down the press for more than delayLongPress milliseconds.
If true, disables all interactions for this component.
Delay in milliseconds from the start of the touch before onPressIn is called.
Delay in milliseconds from the release of the touch before onPressOut is called.
Delay in milliseconds from onPressIn before onLongPress is called.
Extends the touchable area without changing visual layout.<TouchableWithoutFeedback
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
onPress={handlePress}
>
<View><Text>Easier to tap</Text></View>
</TouchableWithoutFeedback>
Defines how far the touch can move off the button before deactivating it.
If true, doesn’t play system sound on touch.
Used to locate this view in end-to-end tests.
TouchableWithoutFeedback also supports accessibility props.
When to Use
Custom Feedback Implementation
When you need to implement custom visual feedback:
function CustomButton() {
const [pressed, setPressed] = React.useState(false);
return (
<TouchableWithoutFeedback
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}
>
<View style={[
styles.button,
pressed && styles.buttonPressed,
]}>
<Text>Custom Feedback</Text>
</View>
</TouchableWithoutFeedback>
);
}
const styles = StyleSheet.create({
button: {
padding: 20,
backgroundColor: '#007AFF',
},
buttonPressed: {
transform: [{ scale: 0.95 }],
backgroundColor: '#0051D5',
},
});
Dismissing Overlays
For closing modals or dropdowns when tapping outside:
function Modal({ visible, onClose, children }) {
return (
<View style={styles.overlay}>
<TouchableWithoutFeedback onPress={onClose}>
<View style={styles.backdrop} />
</TouchableWithoutFeedback>
<View style={styles.content}>
{children}
</View>
</View>
);
}
Keyboard Dismissal
Wrapping content to dismiss keyboard when tapping:
import { Keyboard } from 'react-native';
function FormScreen() {
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.container}>
<TextInput placeholder="Username" />
<TextInput placeholder="Password" secureTextEntry />
<Button title="Login" onPress={handleLogin} />
</View>
</TouchableWithoutFeedback>
);
}
Common Patterns
Image with Custom Overlay
function ImageCard({ source, onPress }) {
const [pressed, setPressed] = React.useState(false);
return (
<TouchableWithoutFeedback
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}
onPress={onPress}
>
<View>
<Image source={source} style={styles.image} />
{pressed && (
<View style={styles.overlay}>
<Icon name="play" size={48} color="white" />
</View>
)}
</View>
</TouchableWithoutFeedback>
);
}
Animated Custom Feedback
function AnimatedButton({ onPress, children }) {
const scaleAnim = useRef(new Animated.Value(1)).current;
const handlePressIn = () => {
Animated.spring(scaleAnim, {
toValue: 0.95,
useNativeDriver: true,
}).start();
};
const handlePressOut = () => {
Animated.spring(scaleAnim, {
toValue: 1,
useNativeDriver: true,
}).start();
};
return (
<TouchableWithoutFeedback
onPressIn={handlePressIn}
onPressOut={handlePressOut}
onPress={onPress}
>
<Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
{children}
</Animated.View>
</TouchableWithoutFeedback>
);
}
Why You Should Avoid It
TouchableWithoutFeedback lacks visual feedback, which:
- Reduces user confidence - users don’t know if their tap registered
- Makes the app feel unresponsive and “web-like”
- Violates platform design guidelines (iOS and Android)
- Decreases accessibility for users with motor impairments
Better Alternatives
Use TouchableOpacity
// Instead of:
<TouchableWithoutFeedback onPress={handlePress}>
<View><Text>Press Me</Text></View>
</TouchableWithoutFeedback>
// Use:
<TouchableOpacity onPress={handlePress}>
<Text>Press Me</Text>
</TouchableOpacity>
Use Pressable
For more control with proper feedback:
<Pressable
onPress={handlePress}
style={({ pressed }) => [
styles.button,
pressed && styles.pressed,
]}
>
{({ pressed }) => (
<Text style={pressed && { opacity: 0.7 }}>
Press Me
</Text>
)}
</Pressable>
Use TouchableHighlight
For underlay color feedback:
<TouchableHighlight
onPress={handlePress}
underlayColor="#DDDDDD"
>
<View><Text>Press Me</Text></View>
</TouchableHighlight>
Accessibility Considerations
If you must use TouchableWithoutFeedback, ensure proper accessibility:
<TouchableWithoutFeedback
accessible={true}
accessibilityRole="button"
accessibilityLabel="Submit form"
accessibilityHint="Submits the registration form"
onPress={handleSubmit}
>
<View style={styles.customButton}>
<Text>Submit</Text>
</View>
</TouchableWithoutFeedback>
Limitations
- Must have exactly one child (wrap multiple children in a View)
- No built-in visual feedback
- Child must be a native component (View, Text, Image, etc.)
- Cannot use with functional components directly