Skip to main content
TouchableOpacity is the most commonly used touchable component in React Native. It responds to touches by decreasing the opacity of the wrapped view, providing subtle visual feedback without layout changes.

Usage

import { TouchableOpacity, Text, StyleSheet } from 'react-native';

function App() {
  return (
    <TouchableOpacity
      onPress={() => console.log('Pressed!')}
      activeOpacity={0.7}
      style={styles.button}
    >
      <Text style={styles.text}>Press Me</Text>
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  button: {
    backgroundColor: '#2196F3',
    padding: 15,
    borderRadius: 8,
    alignItems: 'center',
  },
  text: {
    color: 'white',
    fontSize: 16,
    fontWeight: 'bold',
  },
});

Props

activeOpacity
number
default:"0.2"
Determines what the opacity of the wrapped view should be when touch is active. A value between 0 and 1.
<TouchableOpacity activeOpacity={0.5}>
  <Text>50% opacity on press</Text>
</TouchableOpacity>
onPress
(event: GestureResponderEvent) => void
Called when the touch is released, but not if cancelled (e.g., by a scroll that steals the responder lock).
<TouchableOpacity onPress={() => alert('Tapped!')}>
  <Text>Tap me</Text>
</TouchableOpacity>
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 (finger lifted or moved away).
onLongPress
(event: GestureResponderEvent) => void
Called when the user holds down the press for more than delayLongPress milliseconds.
<TouchableOpacity
  onPress={() => console.log('Pressed')}
  onLongPress={() => console.log('Long pressed')}
  delayLongPress={1000}
>
  <Text>Press or hold me</Text>
</TouchableOpacity>
disabled
boolean
default:"false"
If true, disables all interactions for this component.
<TouchableOpacity disabled={isLoading}>
  <Text>{isLoading ? 'Loading...' : 'Submit'}</Text>
</TouchableOpacity>
delayPressIn
number
default:"0"
Delay in milliseconds from the start of the touch before onPressIn is called.
delayPressOut
number
default:"0"
Delay in milliseconds from the release of the touch before onPressOut is called.
delayLongPress
number
default:"500"
Delay in milliseconds from onPressIn before onLongPress is called.
hitSlop
Insets
Extends the touchable area without changing the visual layout. Useful for making small buttons easier to tap.
<TouchableOpacity
  hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
>
  <Text style={{ fontSize: 12 }}>Small text</Text>
</TouchableOpacity>
pressRetentionOffset
Insets
Defines how far the touch can move off the button before deactivating it.
<TouchableOpacity
  pressRetentionOffset={{ top: 20, left: 20, bottom: 20, right: 20 }}
>
  <Text>Stays active even if finger moves slightly</Text>
</TouchableOpacity>
style
ViewStyleProp
Style applied to the Animated.View wrapper.
testID
string
Used to locate this view in end-to-end tests.
TouchableOpacity also supports accessibility props, focus events, and TV navigation props.

How It Works

TouchableOpacity uses an Animated.View to smoothly animate the opacity:
  • On press: Animates to activeOpacity (default 0.2) with a 150ms duration
  • On release: Animates back to original opacity with a 250ms duration
  • Uses native driver for better performance
Unlike TouchableHighlight, TouchableOpacity doesn’t require your child to have an explicit background color.

Common Patterns

Button Component

function Button({ title, onPress, disabled }) {
  return (
    <TouchableOpacity
      onPress={onPress}
      disabled={disabled}
      style={[
        styles.button,
        disabled && styles.buttonDisabled,
      ]}
    >
      <Text style={styles.buttonText}>{title}</Text>
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  button: {
    backgroundColor: '#007AFF',
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
  },
  buttonDisabled: {
    backgroundColor: '#ccc',
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
    textAlign: 'center',
  },
});

Icon Button

import Icon from 'react-native-vector-icons/Ionicons';

function IconButton({ iconName, onPress, size = 24 }) {
  return (
    <TouchableOpacity
      onPress={onPress}
      hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
    >
      <Icon name={iconName} size={size} color="#333" />
    </TouchableOpacity>
  );
}

Card Component

function Card({ title, description, onPress }) {
  return (
    <TouchableOpacity
      onPress={onPress}
      activeOpacity={0.8}
      style={styles.card}
    >
      <Text style={styles.cardTitle}>{title}</Text>
      <Text style={styles.cardDescription}>{description}</Text>
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  card: {
    backgroundColor: 'white',
    padding: 16,
    borderRadius: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  cardTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 8,
  },
  cardDescription: {
    fontSize: 14,
    color: '#666',
  },
});

List Item

function ListItem({ item, onPress }) {
  return (
    <TouchableOpacity
      onPress={() => onPress(item.id)}
      style={styles.listItem}
    >
      <View style={styles.listItemContent}>
        <Text style={styles.title}>{item.title}</Text>
        <Text style={styles.subtitle}>{item.subtitle}</Text>
      </View>
      <Icon name="chevron-right" size={20} color="#ccc" />
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  listItem: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#eee',
  },
  listItemContent: {
    flex: 1,
  },
  title: {
    fontSize: 16,
    fontWeight: '500',
    marginBottom: 4,
  },
  subtitle: {
    fontSize: 14,
    color: '#666',
  },
});

With Loading State

function AsyncButton({ onPress, title, loading }) {
  return (
    <TouchableOpacity
      onPress={onPress}
      disabled={loading}
      style={[styles.button, loading && styles.buttonLoading]}
    >
      {loading ? (
        <ActivityIndicator color="white" />
      ) : (
        <Text style={styles.buttonText}>{title}</Text>
      )}
    </TouchableOpacity>
  );
}

Custom Press Feedback

function CustomButton() {
  const [pressing, setPressing] = React.useState(false);
  
  return (
    <TouchableOpacity
      onPressIn={() => setPressing(true)}
      onPressOut={() => setPressing(false)}
      activeOpacity={0.6}
    >
      <View style={[styles.button, pressing && styles.buttonPressed]}>
        <Text>{pressing ? 'Pressing...' : 'Press Me'}</Text>
      </View>
    </TouchableOpacity>
  );
}

Platform Considerations

Android Ripple Effect

For Android’s native ripple effect, use TouchableNativeFeedback or Pressable with android_ripple:
import { Pressable, Platform } from 'react-native';

<Pressable
  android_ripple={{ color: 'rgba(0, 0, 0, 0.1)' }}
  style={({ pressed }) => [
    styles.button,
    Platform.OS === 'ios' && pressed && { opacity: 0.7 },
  ]}
>
  <Text>Platform-specific feedback</Text>
</Pressable>

Comparison with Other Touchables

  • TouchableOpacity: Fades opacity (most commonly used)
  • TouchableHighlight: Shows underlay color
  • TouchableWithoutFeedback: No visual feedback
  • Pressable: Modern alternative with more features
For new projects, consider using Pressable which provides more flexibility and follows modern React patterns.

Accessibility

TouchableOpacity automatically sets appropriate accessibility properties:
<TouchableOpacity
  accessible={true}
  accessibilityLabel="Submit form"
  accessibilityHint="Double tap to submit the form"
  accessibilityRole="button"
  onPress={handleSubmit}
>
  <Text>Submit</Text>
</TouchableOpacity>

Build docs developers (and LLMs) love