Crafting Accessible and Animated Buttons in React Native and Expo
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 .
Run the Expo App:
npx expo start
Adjust the initial UI to better reflect our buttons
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:
Let’s onPress
event to make things work:
Let’s press the button to see the action:
Change the Button text color:
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",
},
});
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,
},
});
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
- You can disable a
TouchableOpacity
with certain conditions passingdisabled
props valuetrue
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 />
providepressed
state 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 src
and then create two additional folders inside it:
screens
will hold all the screens of our Appcomponents
will hold different types of components of our App
Let’s create a folder called buttons
inside 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:
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,
},
});
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!