Crafting Accessible and Animated Buttons in React Native and Expo

Md. Jamal Uddin
DevsOrigin
Published in
12 min readDec 7, 2023

--

Photo by Anjan Behera on Unsplash

Welcome to your journey in creating eye-catching and accessible buttons in React Native in an Expo-managed project! In this step-by-step guide, we’ll explore the process of crafting beautiful buttons with engaging animations. This beginner-friendly tutorial will not only walk you through each step but also delve into the uniqueness of various button creation methods and discuss potential pitfalls.

· Getting Started
· <Button />: Quick and Simple
· <TouchableOpacity />: Customizable and Touchable
· <Pressable />: Versatile actions and style options
· Functional Components and Reusable Code
· Animating with react-native-reanimated
· Advantages and Disadvantages
· Additional Resources
· Conclusion

Getting Started

Now, open your terminal and create a new Expo project:

npx create-expo-app rn-expo-animated-buttons
cd rn-expo-animated-buttons

If you face trouble creating a new Expo project, please visit the React Native official development environment setup guide or Expo guide to create your first app where you’ll get the relevant info to get started. I have also published a straightforward guide to Building Your First Mobile App with React Native and Expo

Now, Open your project in your preferred code editor. I am using VS Code, which can be opened from the terminal as follows:

# check current working directory
pwd
# we have already change to the project directory and that why I am seeing below result
# /Users/jaamaalxyz/training/rn-expo-animated-buttons

# open VS Code
code .
App.js file opened in VS Code of our newly created Project

Run the Expo App:

npx expo start
Expo Server Running
The app runs on an iOS simulator via Expo Go

Adjust the initial UI to better reflect our buttons

New UI with a dark background and lighter text color

Check out the following commit on GitHub to see the code at this point.

git checkout 6463f52a5726a15887dff5137696035b4364d3db

<Button />: Quick and Simple

Start with the built-in <Button /> component. It's straightforward but comes with limitations:

Simple button with built-in react-native <Button /> component

Let’s onPress event to make things work:

Simple Button with onPress event

Let’s press the button to see the action:

View the Alert message on the Button press

Change the Button text color:

Changed the Button color to #EEDFEE

Add a wrapper View to make it a real Button

import { StatusBar } from "expo-status-bar";
import { Alert, Button, StyleSheet, View } from "react-native";

export default function App() {
return (
<View style={styles.container}>
<StatusBar style="auto" />
<View style={[styles.wrapper, styles.simpleButton]}>
<Button
title="Simple Button!"
onPress={() => {
Alert.alert("Simple button pressed!");
}}
color={"#EEDFEE"}
/>
</View>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#2D2D2D",
alignItems: "center",
justifyContent: "center",
},
wrapper: {
borderRadius: 30,
paddingVertical: 10,
paddingHorizontal: 20,
},
simpleButton: {
backgroundColor: "#988EAA",
},
});
Button with a wrapper View

Check out the following commit on GitHub to see the code at this point.

git checkout de7567e241594216978ebe9cc42e89c04627db70

<TouchableOpacity />: Customizable and Touchable

For more customization and touch interactions, try <TouchableOpacity /> :

import { StatusBar } from "expo-status-bar";
import {
Alert,
Button,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";

export default function App() {
return (
<View style={styles.container}>
<StatusBar style="auto" />
<View style={[styles.wrapper, styles.simpleButton]}>
<Button
title="Simple Button!"
onPress={() => {
Alert.alert("Simple button pressed!");
}}
color={"#EEDFEE"}
/>
</View>
<View style={{ marginVertical: 10 }} />
<TouchableOpacity style={[styles.wrapper, styles.touchableButton]}>
<Text style={styles.textStyle}>Touchable Button</Text>
</TouchableOpacity>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#2D2D2D",
alignItems: "center",
justifyContent: "center",
},
wrapper: {
borderRadius: 30,
paddingVertical: 10,
paddingHorizontal: 20,
},
simpleButton: {
backgroundColor: "#988EAA",
},
touchableButton: {
backgroundColor: "#004600",
},
textStyle: {
color: "white",
fontSize: 16,
paddingVertical: 10,
},
});
Creating another Button using Touchable Opacity

Add onPress event to do something on the button click

import { StatusBar } from 'expo-status-bar';
import {
Alert,
Button,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';

export default function App() {
return (
<View style={styles.container}>
<StatusBar style="auto" />
<View style={[styles.wrapper, styles.simpleButton]}>
<Button
title="Simple Button!"
onPress={() => {
Alert.alert('Simple button pressed!');
}}
color={'#EEDFEE'}
/>
</View>
<View style={{ marginVertical: 10 }} />
<TouchableOpacity
onPress={() => {
Alert.alert('Touchable button pressed!');
}}
style={[styles.wrapper, styles.touchableButton]}
>
<Text style={styles.textStyle}>Touchable Button</Text>
</TouchableOpacity>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#2D2D2D',
alignItems: 'center',
justifyContent: 'center',
},
wrapper: {
borderRadius: 30,
paddingVertical: 10,
paddingHorizontal: 20,
},
simpleButton: {
backgroundColor: '#988EAA',
},
touchableButton: {
backgroundColor: '#004600',
},
textStyle: {
color: 'white',
fontSize: 16,
paddingVertical: 10,
},
});

Press the Touchable button to see the action

Showing an Alert on Touchable Button Press
  • You can disable a TouchableOpacity with certain conditions passing disabled props value true to handle the button activity
  • You can also able to set dynamic activeOpacity based on the activity

To see the code at this stage, please check out the following commit on GitHub.

git checkout fc5db3a83ff53c14950ccac58670c2530d73254a

<Pressable />: Versatile actions and style options

Explore the versatility of <Pressable /> creating complex interactions and animations.

import { StatusBar } from 'expo-status-bar';
import {
Alert,
Button,
Pressable,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';

export default function App() {
return (
<View style={styles.container}>
<StatusBar style="auto" />
<View style={[styles.wrapper, styles.simpleButton]}>
<Button
title="Simple Button!"
onPress={() => {
Alert.alert('Simple button pressed!');
}}
color={'#EEDFEE'}
/>
</View>
<View style={{ marginVertical: 10 }} />
<TouchableOpacity
onPress={() => {
Alert.alert('Touchable button pressed!');
}}
disabled={false}
activeOpacity={0.6}
style={[styles.wrapper, styles.touchableButton]}
>
<Text style={styles.textStyle}>Touchable Button</Text>
</TouchableOpacity>
<View style={{ marginVertical: 10 }} />
<Pressable
onPress={() => {
Alert.alert('Pressable button pressed!');
}}
>
{({ pressed }) => (
<View
style={[styles.wrapper,{ backgroundColor: pressed ? '#0476a0' : '#1146aa' },]}
>
<Text
style={[styles.textStyle,{ color: pressed ? '#fafafa' : '#aaffaa' },]}
>
Pressable Button
</Text>
</View>
)}
</Pressable>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#2D2D2D',
alignItems: 'center',
justifyContent: 'center',
},
wrapper: {
borderRadius: 30,
paddingVertical: 10,
paddingHorizontal: 20,
},
simpleButton: {
backgroundColor: '#988EAA',
},
touchableButton: {
backgroundColor: '#004600',
},
textStyle: {
color: 'white',
fontSize: 16,
paddingVertical: 10,
},
});
Pressable Button with style on onPress event
  • <Pressable /> provide pressedstate that we can use to make our components more interactive by conditionally rendering different components and styles.
  • <Pressable /> have several useful props that can make our lives easier by making our components more versatile.

Official Pressable Documentation: https://reactnative.dev/docs/pressable

To view the code at this point, please check out the following commit on GitHub.

git checkout aa83304fd6b67db556c4fd936a59fdf0a04c57c5

Functional Components and Reusable Code

Embrace functional components and reusable code for a cleaner and maintainable project structure. Let’s create several files and folders to organize our app and reuse code:

Let’s create a folder source folder called srcand then create two additional folders inside it:

  • screens will hold all the screens of our App
  • components will hold different types of components of our App

Let’s create a folder called buttonsinside our components folder that will hold all the button components:

  • src/components/buttons/SimpleButton.js: A button with the <Button /> component
import React from 'react';
import { Alert, Button, StyleSheet, View } from 'react-native';

const SimpleButton = ({ title, message, style }) => {
return (
<View style={[styles.bgColor, style]}>
<Button
title={title}
onPress={() => Alert.alert(message)}
color={'#EEDFEE'}
/>
</View>
);
};

export default SimpleButton;

const styles = StyleSheet.create({
bgColor: {
backgroundColor: '#988EAA',
},
});
  • src/components/buttons/TouchableButton.js: A button with the <TouchableOpacity /> component
import React from 'react';
import { Alert, StyleSheet, Text, TouchableOpacity } from 'react-native';

const TouchableButton = ({ title, message, style }) => {
return (
<TouchableOpacity
onPress={() => Alert.alert(message)}
disabled={false}
activeOpacity={0.6}
style={[styles.bgColor, style]}
>
<Text style={styles.textStyle}>{title}</Text>
</TouchableOpacity>
);
};

export default TouchableButton;

const styles = StyleSheet.create({
bgColor: {
backgroundColor: '#004600',
},
textStyle: {
color: 'white',
fontSize: 16,
paddingVertical: 10,
},
});
  • src/components/buttons/PressableButton.js: Button with the <Pressable /> component
import React from 'react';
import { Alert, Pressable, StyleSheet, Text, View } from 'react-native';

const PressableButton = ({ title, message, style }) => {
return (
<Pressable
onPress={() => {
Alert.alert(message);
}}
>
{({ pressed }) => (
<View
style={[{ backgroundColor: pressed ? '#0476a0' : '#1146aa' }, style]}
>
<Text
style={[
{ color: pressed ? '#fafafa' : '#aaffaa' },
styles.textStyle,
]}
>
{title}
</Text>
</View>
)}
</Pressable>
);
};

export default PressableButton;

const styles = StyleSheet.create({
textStyle: {
color: 'white',
fontSize: 16,
paddingVertical: 10,
},
});

Let’s create an index.js file to export all the buttons from here so that we can directly import any button from components/buttons

  • src/components/buttons/index.js
import SimpleButton from './SimpleButton';
import PressableButton from './PressableButton';
import TouchableButton from './TouchableButton';

export { SimpleButton, PressableButton, TouchableButton };

Let’s create another folder inside components called spacer that will hold all the different spacer components that we can reuse in between different buttons to create better visualization:

  • src/components/spacer/Spacer.js : A default spacer component where we will be passed different props to create horizontal or vertical space between two components:
import React from 'react';
import { View } from 'react-native';

const Spacer = ({ vSize = 0, hSize = 0 }) => {
return (
<View
style={{
paddingVertical: vSize,
paddingHorizontal: hSize,
}}
/>
);
};

export default Spacer;
  • src/components/spacer/VerticalSpacer.js: Add some space between two components:
import React from 'react';
import Spacer from './Spacer';

const VerticalSpacer = ({ size = 10 }) => {
return <Spacer vSize={size} />;
};

export default VerticalSpacer;

we’ve passed a default value in this spacing component to omit to pass the same value multiple times, but if we need different spacing, we can pass that value.

Export all spacer from their index.js file:

  • src/components/spacer/index.js
import Spacer from './Spacer';
import VerticalSpacer from './VerticalSpacer';

export { Spacer, VerticalSpacer };

Let’s create Home screen inside screens folder:

  • src/screens/Home.js : A screen file that holds all the components that are showing on our App screen
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, View } from 'react-native';
import {
PressableButton,
SimpleButton,
TouchableButton,
} from '../components/buttons';
import { VerticalSpacer } from '../components/spacer';

const Home = () => {
return (
<View style={styles.container}>
<StatusBar style="auto" />
<SimpleButton
title={'Simple Button!'}
message={'Simple button pressed!'}
style={styles.wrapper}
/>
<VerticalSpacer />
<TouchableButton
title={'Touchable Button'}
message={'Touchable button pressed!'}
style={styles.wrapper}
/>
<VerticalSpacer />
<PressableButton
title={'Pressable Button'}
message={'Pressable button pressed!'}
style={styles.wrapper}
/>
</View>
);
};

export default Home;

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#2D2D2D',
alignItems: 'center',
justifyContent: 'center',
},
wrapper: {
borderRadius: 30,
paddingVertical: 10,
paddingHorizontal: 20,
},
});

Export Home via screens/index.js as below:

  • src/screens/index.js
import Home from './Home';

export { Home };

And finally here is our App.js now:

import React from 'react';
import { Home } from './src/screens';

export default function App() {
return <Home />;
}

The representation of UI remains the same as below:

final app view with 3 different buttons

To view the code at this stage, please check out the following commit on GitHub.

git checkout 9caffb8c83952c6c2ea4be0cea5dfb595ccd0a28

Animating with react-native-reanimated

Make our buttons come alive with animations using the react-native-reanimated library. Install it with:

npx expo install react-native-reanimated

Add react-native-reanimated/plugin a plugin to your babel.config.js.

  module.exports = {
presets: [
... // don't add it here :)
],
plugins: [
...
'react-native-reanimated/plugin',
],
};

Clear Metro bundler cache (recommended) and start Expo server:

npx expo start -c

Now, add delightful animations to our buttons for an engaging experience:

Let’s create a new button called AnimatedButton

  • src/components/buttons/AnimatedButton.js
import React from 'react';
import { Alert, StyleSheet, TouchableOpacity } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';

const AnimatedButton = ({ title, message }) => {
const scale = useSharedValue(60);

const animatedStyle = useAnimatedStyle(() => {
return {
paddingHorizontal: withSpring(scale.value),
paddingVertical: withSpring(scale.value / 3, { stiffness: 10 }),
};
});

const animatedTextStyle = useAnimatedStyle(() => {
return {
fontSize: withSpring(scale.value / 3, { stiffness: 30 }),
};
});

return (
<TouchableOpacity
onPress={() => Alert.alert(message)}
onPressIn={() => (scale.value = 20)}
onPressOut={() => (scale.value = 60)}
>
<Animated.View style={[styles.wrapper, animatedStyle]}>
<Animated.Text style={[styles.textStyle, animatedTextStyle]}>
{title}
</Animated.Text>
</Animated.View>
</TouchableOpacity>
);
};

export default AnimatedButton;

const styles = StyleSheet.create({
wrapper: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 10,
backgroundColor: 'red',
borderRadius: 30,
},
textStyle: {
color: 'white',
fontSize: 16,
},
});

Call our newly created AnimatedButton inside Home screen

import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, View } from 'react-native';
import {
AnimatedButton,
PressableButton,
SimpleButton,
TouchableButton,
} from '../components/buttons';
import { VerticalSpacer } from '../components/spacer';

const Home = () => {
return (
<View style={styles.container}>
<StatusBar style="auto" />
<SimpleButton
title={'Simple Button!'}
message={'Simple button pressed!'}
style={styles.wrapper}
/>
<VerticalSpacer size={20} />
<TouchableButton
title={'Touchable Button'}
message={'Touchable button pressed!'}
style={styles.wrapper}
/>
<VerticalSpacer />
<PressableButton
title={'Pressable Button'}
message={'Pressable button pressed!'}
style={styles.wrapper}
/>
<VerticalSpacer size={15} />
<AnimatedButton
title={'Animated Button'}
message={'Animated Button clicked!'}
/>
</View>
);
};

export default Home;

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#2D2D2D',
alignItems: 'center',
justifyContent: 'center',
},
wrapper: {
borderRadius: 30,
paddingVertical: 10,
paddingHorizontal: 20,
},
});
Animated button with Red color

Click the button to see the action.

To see the code at this point, please take a look at the following commit on GitHub.

git checkout ffb52b1615dad329500c24e1c8eaa7b9bb1b6f73

Advantages and Disadvantages

<Button>: Simple and easy to use

Advantages:

  • Renders consistently across platforms.
  • Provides basic accessibility features.
  • Doesn’t require additional styling.

Disadvantages:

  • Limited customization options.
  • Can’t change the underlying platform-specific button style.
  • Limited accessibility features compared to other options.

<TouchableOpacity>: Highly customizable

Advantages:

  • Can change the underlying platform-specific button style.
  • More accessible than the basic Button component.

Disadvantages:

  • More complex to use than the <Button /> component.
  • Requires additional styling to achieve a button-like appearance.

<Pressable>: Most customizable option

Advantages:

  • Supports most gesture types.
  • Offers the most accessibility features.

Disadvantages:

  • Most complex to use.
  • Requires the most effort to style.
  • May require additional libraries for accessibility features.

Choosing the right component:

The best component for your button depends on your specific needs and requirements. Here are some general guidelines:

  • Use <Button> if you need a simple button with minimal customization and platform consistency.
  • Use <TouchableOpacity> if you need a customizable button with more control over the appearance and accessibility.
  • Use <Pressable> if you need the most customization and accessibility features, or if you need to support advanced gestures.

Additional factors to consider:

  • Performance: <Button> is generally the most performant option, followed by <TouchableOpacity> and <Pressable>.
  • Plans: React Native plans to gradually deprecate <TouchableOpacity> and encourage the use of <Pressable> for new projects.

Ultimately, the best way to choose the right component is to experiment and see which one best meets your needs.

Additional Resources

To further enhance your button creation skills and React Native expertise, explore these resources:

GitHub repo of this article: https://github.com/jaamaalxyz/rn-expo-animated-buttons

Credits:

This article is heavily inspired by Kadi Kraman's video course Creating Buttons in React Native with Three Levels of Customization on Egghead

Conclusion

In this guide, we explored various ways to create accessible and beautifully animated buttons in React Native and Expo. We covered different components, added animations, followed best practices with functional components, and provided meaningful feedback for button clicks. Choose the approach that best fits your project requirements, and Keep exploring, experimenting, and building to solidify your skills.

Happy coding!

--

--

Md. Jamal Uddin
DevsOrigin

Software engineer passionate about building and delivering SaaS apps using React Native. Agile enthusiast who occasionally writes blog posts about life and tech