Supercharging your Pressables in React Native

Published on Apr 27, 20254 min read

If you've developed React Native applications before, you certainly used the <Button /> component at some point and realized how limited it is, since the <Button /> component uses the native style and doesn't allow for much customization.

You might have turned to the <TouchableOpacity /> or <TouchableHighlight /> components to create buttons that look more like the ones you see in different apps. These components are more flexible and allow you to customize the look and feel of your buttons to match your app's design, they also provide a better user experience by giving users a visual feedback when they press the button.

But then when React Native v0.63 was released, the <Pressable /> component was introduced, and it's the recommended way to handle user interactions within your app. Although it improved the API for handling touch events, it doesn't provide all the built-in styles and effects that the Touchables do.

You can read more about why the <Pressable /> component was introduced on the React Native v0.63 announcement blog post.

Setup project

I'll be using Expo and React Native Reanimated to create the examples in this post, but you can translate the code to use the React Native Animated API if you prefer.

To set up this project, I ran the following command:

bash
bun create expo pressables --template blank-typescript
cd pressables
bun expo install react-native-reanimated

bun ios

Pressable with opacity

Let's start by creating the same visual feedback that the <TouchableOpacity /> component provides, which is the opacity effect when the user presses the button.

app.tsx
import {
  Pressable,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from "react-native";
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from "react-native-reanimated";

const AnimatedPressable = Animated.createAnimatedComponent(Pressable);

export default function App() {
  const pressed = useSharedValue(false);

  const animatedStyle = useAnimatedStyle(() => {
    const opacity = pressed.value
      ? 0.2
      : withTiming(1, {
          duration: 200,
        });

    return {
      opacity: opacity,
      backgroundColor: "purple",
      padding: 20,
      borderRadius: 5,
      width: 150,
      justifyContent: "center",
      alignItems: "center",
    };
  });

  return (
    <View style={styles.container}>
      <TouchableOpacity
        style={{
          backgroundColor: "purple",
          padding: 20,
          borderRadius: 5,
          width: 150,
          justifyContent: "center",
          alignItems: "center",
        }}
      >
        <Text style={styles.buttonText}>Touchable</Text>
      </TouchableOpacity>
      <AnimatedPressable
        style={animatedStyle}
        onPressIn={() => {
          pressed.value = true;
        }}
        onPressOut={() => {
          pressed.value = false;
        }}
      >
        <Text style={styles.buttonText}>Pressable</Text>
      </AnimatedPressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    gap: 10,
    justifyContent: "center",
  },
  buttonText: {
    fontSize: 20,
  },
});

With the code above we created a custom <Pressable /> component that has the same opacity effect as the <TouchableOpacity /> component, check it out:

Pressable with scale

This is one of my favorites effects, it gives a nice feedback to the user when they press the button, and it's very easy to implement. Let's cut to the chase and see the code:

app.tsx
import {
  Pressable,
  StyleSheet,
  Text,
  View,
} from "react-native";
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from "react-native-reanimated";

const AnimatedPressable = Animated.createAnimatedComponent(Pressable);

export default function App() {
  const pressed = useSharedValue(false);

  const animatedStyle = useAnimatedStyle(() => {
    const scale = withTiming(pressed.value ? 0.92 : 1, {
      duration: 100,
    });

    return {
      transform: [{ scale }],
      backgroundColor: "darkblue",
      padding: 20,
      borderRadius: 5,
      width: 150,
      justifyContent: "center",
      alignItems: "center",
    };
  });

  return (
    <View style={styles.container}>
      <AnimatedPressable
        style={animatedStyle}
        onPressIn={() => {
          pressed.value = true;
        }}
        onPressOut={() => {
          pressed.value = false;
        }}
      >
        <Text style={styles.buttonText}>Pressable</Text>
      </AnimatedPressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    gap: 10,
    justifyContent: "center",
  },
  buttonText: {
    fontSize: 20,
    color: 'white',
    fontWeight: 'bold',
  },
});

And now the result:

Pressable with haptic feedback

This is a very underused feature in React Native, but it's very easy to implement, specially with the help of the Expo Haptics library.

Apple has a great article on best practices for haptic feedback in iOS apps, and I highly recommend you check it out: Playing Haptics.

Unfortunately, I can't show you the haptic feedback on video, but you can try it out on your own device. So let's see the code, but first, make sure to install the Expo Haptics library:

bash
bun expo install expo-haptics

And now the code:

app.tsx
import {
  Pressable,
  StyleSheet,
  Text,
  View,
} from "react-native";

export default function App() {
  return (
    <View style={styles.container}>
      <Pressable
        style={styles.button}
        onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}
      >
        <Text style={styles.buttonText}>Light</Text>
      </Pressable>
      <Pressable
        style={styles.button}
        onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium)}
      >
        <Text style={styles.buttonText}>Medium</Text>
      </Pressable>
      <Pressable
        style={styles.button}
        onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)}
      >
        <Text style={styles.buttonText}>Heavy</Text>
      </Pressable>
      <Pressable
        style={styles.button}
        onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Rigid)}
      >
        <Text style={styles.buttonText}>Rigid</Text>
      </Pressable>
      <Pressable
        style={styles.button}
        onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Soft)}
      >
        <Text style={styles.buttonText}>Soft</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    gap: 10,
    justifyContent: "center",
  },
  button: {
    backgroundColor: "lightblue",
    padding: 10,
    borderRadius: 5,
    width: 150,
    alignItems: "center",
  },
  buttonText: {
    fontSize: 20,
    color: "darkblue",
    fontWeight: "bold",
  },
});

I really like using haptic feedback on iOS, it gives a nice touch to the app, and if you combine with long press, it can be a great way to show the user that they can do something else with the button, rewarding them with a nice feeling.

Conclusion

This is some ways to supercharge your <Pressable /> components in React Native, and I hope you found this post helpful.

If you have any questions or suggestions, feel free to reach out to me, you can find my socials at the home page of my blog.