Build a Custom Modal with the Animated API in React Native
Creating a better user experience is one of the most important aspects of any application. This is where animations in mobile applications come into play. Animations are an important part of your mobile application.
Fortunately, for React Native developers, there is recommended a way to create desirable user experiences. It can be achieved through Animated API. For most use cases Animated API provides the best use case to design and create fluid animations.
In this tutorial, you are going to take dive deep in creating a custom modal and animated it on a button click. This is the final result we looking to achieve in a React Native application.
Table of Contents
🔗- Prerequisites
- Setup up Screens
- Setting up Redux
- Creating an animated Custom Modal
- Integrating Custom Modal to HomeScreen
- Animating HomeScreen
- Conclusion
Prerequisites
🔗Here is a complete list of plugins, packages, and tools that you’re going to need in order to follow along.
- Nodejs >=
v8.x.x
installed along with npm/yarn. watchman
, the file change watcher for React Native projects.- Expo CLI >=
v2.19.4
.
To get started create a new project using expo-cli
toolchain with the following set of commands. The first command will create a new project directory. Make sure you are inside the project when running the application in a simulator environment or a real device.
# create a new bare projectexpo-cli init rn-animations# navigate inside the directorycd rn-animations# to start the projectyarn start
We are using yarn
to kickstart the app but you are most welcome to use npm or npm scrips or use Expo CLI tool command: expo start
. This way you can verify that the project has been created successfully.
Next step is to install different dependencies or libraries that our little demo project is going to depend. Run the below command from a terminal window.
yarn add redux react-redux styled-components @expo/vector-icons
After installing these dependencies, this is how package.json
file looks like.
1{2 "main": "node_modules/expo/AppEntry.js",3 "scripts": {4 "start": "expo start",5 "android": "expo start --android",6 "ios": "expo start --ios",7 "web": "expo start --web",8 "eject": "expo eject"9 },10 "dependencies": {11 "@expo/vector-icons": "^10.0.2",12 "expo": "^33.0.0",13 "react": "16.8.3",14 "react-dom": "^16.8.6",15 "react-native": "https://github.com/expo/react-native/archive/sdk-33.0.0.tar.gz",16 "react-native-web": "^0.11.4",17 "react-redux": "^7.0.3",18 "redux": "^4.0.1",19 "styled-components": "^4.3.1"20 },21 "devDependencies": {22 "babel-preset-expo": "^5.1.1"23 },24 "private": true25}
Setup up Screens
🔗Create two screens inside a new directory called screens/
. One is going to be the home screen for the app and the main UI point. The second screen is going to be a normal screen but will behave like a custom modal in terms of UI behavior. You can name them whatever you like but make sure to remember those names.
Here is the initial snippet of screens/HomeScreen.js
.
1import React from 'react';2import styled from 'styled-components';34class HomeScreen extends React.Component {5 render() {6 return (7 <Container>8 <ButtonText>Open Modal</ButtonText>9 </Container>10 );11 }12}1314const Container = styled.View`15 flex: 1;16 justify-content: center;17 align-items: center;18`;1920const ButtonText = styled.Text`21 font-size: 20px;22 font-weight: 600;23`;2425export default HomeScreen;
The above snippet is using styled-components
to define new UI elements using React Native API. For more information on to read on how to integrate styled-components
and its advantage in a React Native app, please go through this link.
For CustomModal.js
:
1import React from 'react';2import styled from 'styled-components';34class CustomModal extends React.Component {5 render() {6 return (7 <Container>8 <Text>CustomModal</Text>9 </Container>10 );11 }12}1314const Container = styled.View`15 flex: 1;16 justify-content: center;17 align-items: center;18`;1920const Text = styled.Text`21 font-size: 20px;22 font-weight: 600;23`;2425export default CustomModal;
Now, let us import the HomeScreen
component inside App.js
. This component is going to be the entry point of our app.
1import React from 'react';23import HomeScreen from './screens/HomeScreen';45export default function App() {6 return <HomeScreen />;7}
On running the application using yarn start
you will get the following result. The header has a breakthrough line indicates that the stack navigator has been integrated into our app.
Setting up Redux
🔗In this section, let us create a simple reducer for Redux state management library. It might be that redux as a library is overkill for the purpose of this tutorial, so if you don't want to use it, please find a way that works for you. Also, I am not going to get into details of how you should manage reducers and actions right now. That said, first create a reducer inside a new file called reducers/index.js
with an initial state.
1const initialState = {2 action: ''3};45const reducer = (state = initialState, action) => {6 switch (action.type) {7 case 'OPEN_MODAL':8 return { ...state, action: 'openModal' };9 case 'CLOSE_MODAL':10 return { ...state, action: 'closeModal' };11 default:12 return state;13 }14};1516export default reducer;
Since the redux
and react-redux
dependencies are already installed, open App.js
file and inside write the code to hook a store provider for redux to manage global state in the app.
1import React from 'react';2import { createStore } from 'redux';3import { Provider } from 'react-redux';4import HomeScreen from './screens/HomeScreen';5import reducer from './reducers';67const store = createStore(reducer);89const App = () => (10 <Provider store={store}>11 <HomeScreen />12 </Provider>13);1415export default App;
The redux setup is complete. Let us move on to the next section where the real thing starts.
Creating an animated Custom Modal
🔗Even though we are creating this custom modal as a screen, you can always use this as a re-usable component. Open CustomModel.js
file and add the following snippet of code.
1import React from 'react';2import styled from 'styled-components';34class CustomModal extends React.Component {5 render() {6 return (7 <Container>8 <Header />9 <Body />10 </Container>11 );12 }13}1415const Container = styled.View`16 position: absolute;17 background: white;18 width: 100%;19 height: 100%;20 z-index: 100;21`;2223const Header = styled.View`24 background: #333;25 height: 150px;26`;2728const Body = styled.View`29 background: #eaeaea;30 height: 900px;31`;3233export default CustomModal;
The above component is simple. It contains three react native views. On the Container
we are using the CSS property position: absolute
. The z-index
will allow the modal to appear on the top of the HomeScreen
component. The Header
and the Body
are subviews with fixed height
.
In order to see this in action, open HomeScreen.js
and import it.
1// ...2import CustomModal from './CustomModal';34class HomeScreen extends React.Component {5 render() {6 return (7 <Container>8 <CustomModal />9 <ButtonText>Open Modal</ButtonText>10 </Container>11 );12 }13}1415// ...
You will get the following result in your simulator.
Great! Now that we can see the Custom Model on the screen, let us start applying some animations. To apply animations in this demo application, we will be using Animated
API from React Native. You do not have to install anything rather than import the API from React Native core. Open CustomModel.js
and modify it. In the below snippet, also define an initial state.
This initial state value defines an Animated top
value to push model up and down.
1import React from 'react';2import styled from 'styled-components';3import { Animated } from 'react-native';45class CustomModal extends React.Component {6 state = {7 top: new Animated.Value(900)8 };9 render() {10 return (11 <AnimatedContainer style={{ top: this.state.top }}>12 <Header />13 <Body />14 </AnimatedContainer>15 );16 }17}1819const Container = styled.View`20 position: absolute;21 background: white;22 width: 100%;23 height: 100%;24 z-index: 100;25`;2627const AnimatedContainer = Animated.createAnimatedComponent(Container);2829const Header = styled.View`30 background: #333;31 height: 150px;32`;3334const Body = styled.View`35 background: #eaeaea;36 height: 900px;37`;3839export default CustomModal;
Right now, the initial top value is receiving an Animated value of 900
. The syntax Animated.Value()
is used to bind style properties such as we are using with AnimatedContainer
. In order to perform animations, the component or the View
has to be Animated, thus, you can Animated.createAnimatedComponent()
to transform a basic View
an Animated one.
Next, define a custom method called toggleModal
before the render function. This function will handle the animations to open and close the modal. So far, it is:
1componentDidMount() {2 this.toggleModal()3 }45toggleModal = () => {6 Animated.spring(this.state.top, {7 toValue: 1748 }).start()9}
In the above snippet, we are using spring animations using Animated.spring()
method. This is used to configure animations based on the analytical values to create a simple spring model based on physics. To read more about this method, take a look at this link in official React Native documentation. The toValue
is passed as the second parameter. Lastly, to start an animation, you need to call the method .start()
.
To trigger this animation on the first render of the component CustomModal
, we are using React's lifecycle method componentDidMount()
.
You will get the following result.
We need to add a button to close the modal. Let us add the styles and view for the close button on the modal. Create a CloseView
component with styled-components
library inside a TouchableOpacity
button. Also, for the close icon, we are going to use @expo/vector-icons
library.
1import React from 'react';2import styled from 'styled-components';3import { Animated, TouchableOpacity, Dimensions } from 'react-native';4import * as Icon from '@expo/vector-icons';56const screenHeight = Dimensions.get('window').height;78class CustomModal extends React.Component {9 state = {10 top: new Animated.Value(screenHeight)11 };1213 componentDidMount() {14 this.toggleModal();15 }1617 toggleModal = () => {18 Animated.spring(this.state.top, {19 toValue: 17420 }).start();21 };2223 closeModal = () => {24 Animated.spring(this.state.top, {25 toValue: screenHeight26 }).start();27 };2829 render() {30 return (31 <AnimatedContainer style={{ top: this.state.top }}>32 <Header />33 <TouchableOpacity34 onPress={this.closeModal}35 style={{36 position: 'absolute',37 top: 120,38 left: '50%',39 marginLeft: -22,40 zIndex: 141 }}42 >43 <CloseView style={{ elevation: 10 }}>44 <Icon.Ionicons name="ios-close" size={44} color="blue" />45 </CloseView>46 </TouchableOpacity>47 <Body />48 </AnimatedContainer>49 );50 }51}5253const Container = styled.View`54 position: absolute;55 background: white;56 width: 100%;57 height: 100%;58 z-index: 100;59`;6061const AnimatedContainer = Animated.createAnimatedComponent(Container);6263const Header = styled.View`64 background: #333;65 height: 150px;66`;6768const Body = styled.View`69 background: #eaeaea;70 height: ${screenHeight};71`;7273const CloseView = styled.View`74 width: 44px;75 height: 44px;76 border-radius: 22px;77 background: white;78 justify-content: center;79 align-items: center;80 box-shadow: 0 5px 10px rgba(0, 0, 0, 0.5);81`;8283export default CustomModal;
To calculate the height of a screen's device, in the above snippet, start by importing Dimensions
API. React Native uses Dots Per Inch (DPI) to measure the size (width and height) of a device's screen. Dimensions.get("window").height
allows to gather the screen height. We then use this screenHeight
variable in three places. First, the initial state which was before had a static value of 900
is now able to adapt for different devices.
Second, to close the modal or inside closeModal()
method. In the toggleModal
function we are setting a custom to value of 174
which leaves a partial view of the HomeScreen
in the background. If you set this value to 0
, the custom modal will cover the whole screen. To close the modal is setting this value to default screen's height. The TouchableOpacity
that wraps the close button invokes the method closeModal
.
The third place where the variable screenHeight
are the styles of the view container: Body
. Please note that box-shadow will not work on Android devices. If you still want to give the close button a shadow, use elevation
property as inline styles to CloseView
.
You will get the following result in your simulator device.
Integrating Redux to Modal
🔗In this section, you are going to use Redux to manage the state of opening and closing the modal. We have already defined the reducers and actions to serve this purpose. Open CustomModal.js
and import the connect
Hight Order Function react-redux
library. After that, create two new functions that are somewhat boilerplate code when using a redux in any React or React Native application. These functions are called: mapStateToProps()
and mapDispatchToProps()
.
1// ...2import { connect } from 'react-redux';34function mapStateToProps(state) {5 return { action: state.action };6}78function mapDispatchToProps(dispatch) {9 return {10 closeModal: () =>11 dispatch({12 type: 'CLOSE_MODAL'13 })14 };15}1617export default connect(mapStateToProps, mapDispatchToProps)(CustomModal);
Next, let us merge the business logic to trigger animations for opening and closing the modal inside the same toggleModal
function. The below snippet uses if
statements to track the right action coming from the global state.
1toggleModal = () => {2 if (this.props.action === 'openModal') {3 Animated.spring(this.state.top, {4 toValue: 1745 }).start();6 }7 if (this.props.action === 'closeModal') {8 Animated.spring(this.state.top, {9 toValue: screenHeight10 }).start();11 }12};
Also, change the value for onPress
attribute at the TouchableOpacity
to onPress={this.props.closeMenu}
. Lastly, componentDidMount()
method is going to call toggleModal()
only on the initial render which means it is going to be called only once. To resolve this, let us use componentDidUpdate()
. This lifecycle method triggers every time there is a new state or change in props.
1componentDidUpdate() {2 this.toggleModal()3 }
Integrating Custom Modal to HomeScreen
🔗Since the initial state at the application level right now is empty, you are not going to see the modal trigger, by itself, when you refresh the Expo app. This serves the purpose of keeping the default behavior of the modal to be closed. But top open this custom modal, we are going to add a button on the HomeScreen
to activate it.
Open HomeScreen.js
and connect it to the redux state like below.
1import React from 'react';2import { TouchableOpacity } from 'react-native';3import styled from 'styled-components';4import { connect } from 'react-redux';5import CustomModal from './CustomModal';67class HomeScreen extends React.Component {8 render() {9 return (10 <Container>11 <CustomModal />12 <TouchableOpacity onPress={this.props.openModal}>13 <ButtonText>Open Modal</ButtonText>14 </TouchableOpacity>15 </Container>16 );17 }18}1920const Container = styled.View`21 flex: 1;22 justify-content: center;23 align-items: center;24`;2526const ButtonText = styled.Text`27 font-size: 20px;28 font-weight: 600;29`;3031function mapStateToProps(state) {32 return { action: state.action };33}3435function mapDispatchToProps(dispatch) {36 return {37 openModal: () =>38 dispatch({39 type: 'OPEN_MODAL'40 })41 };42}4344export default connect(mapStateToProps, mapDispatchToProps)(HomeScreen);
Click the button Open Modal
on the UI screen and you will get similar results as follows.
Congratulations! You have just created a custom model that is animated and integrated it from another screen. You can end this tutorial right here if it serves the purpose or the as the title suggests. Though, if you want to continue, let us add some animations to the HomeScreen to create a pleasing UI in the next section.
Animating HomeScreen
🔗In the HomeScreen
component we are going to import quite a few APIs from React Native. The result we are trying to achieve is as follows. It will be easier for you to view what we want to happen to understand the code in this section.
Now that you have seen that let us first go through what are we going to import from react-native
.
1// ...2import {3 TouchableOpacity,4 StatusBar,5 Animated,6 Easing,7 Platform8} from 'react-native';
In the above demo, we are switching between status bar's color from dark to light when the modal opens, we are going to use StatusBar
inside componentDidMount()
.
1 componentDidMount() {2 StatusBar.setBarStyle("dark-content", true)34 if (Platform.OS == "android") {5 StatusBar.setBarStyle("light-content", true)6 }7 }
Next, we define an initial state to manage Animations with two properties, scale
and opacity
.
1state = {2 scale: new Animated.Value(1),3 opacity: new Animated.Value(1)4};
The create a toggleModal
method where most of the things are happening. It gets triggered by componentDidUpdate()
lifecycle method just like in the CustomModal
component.
1componentDidUpdate() {2 this.toggleModal()3 }45 toggleModal = () => {6 if (this.props.action === "openModal") {7 Animated.timing(this.state.scale, {8 toValue: 0.9,9 duration: 300,10 easing: Easing.in()11 }).start()12 Animated.spring(this.state.opacity, {13 toValue: 0.514 }).start()1516 StatusBar.setBarStyle("light-content", true)17 }1819 if (this.props.action === "closeModal") {20 Animated.timing(this.state.scale, {21 toValue: 1,22 duration: 300,23 easing: Easing.in()24 }).start()25 Animated.spring(this.state.opacity, {26 toValue: 127 }).start()2829 StatusBar.setBarStyle("dark-content", true)30 }31 }
To trigger the effect HomeScreen
shrinking in the background when the modal opens, is achieved by using Animated.timing()
. This method maps time range to an easing
value. This easing
value triggers the Easing
module from react native core. This module implements common visualization motions such as bounce, elastic, in (which we are using) and out, cubic, sin, back, ease, linear, quad, inout and many more. To get complete information about Easing, please refer to the docs here.
The Animated.timing()
has a default value of 500
milliseconds. We are changing it to 300
.
To create partial opacity when the home screen shrinks in the background, we are again using spring animations. Depending on whether the modal is being opened or closed, the style of the StatusBar
is being changed by calling the StatusBar.setBarStyle()
method.
Here is the complete code for HomeScreen.js
file.
1import React from 'react';2import {3 TouchableOpacity,4 StatusBar,5 Animated,6 Easing,7 Platform8} from 'react-native';9import styled from 'styled-components';10import { connect } from 'react-redux';11import CustomModal from './CustomModal';1213class HomeScreen extends React.Component {14 state = {15 scale: new Animated.Value(1),16 opacity: new Animated.Value(1)17 };1819 componentDidMount() {20 StatusBar.setBarStyle('dark-content', true);2122 if (Platform.OS == 'android') {23 StatusBar.setBarStyle('light-content', true);24 }25 }2627 componentDidUpdate() {28 this.toggleModal();29 }3031 toggleModal = () => {32 if (this.props.action === 'openModal') {33 Animated.timing(this.state.scale, {34 toValue: 0.9,35 duration: 300,36 easing: Easing.in()37 }).start();38 Animated.spring(this.state.opacity, {39 toValue: 0.540 }).start();4142 StatusBar.setBarStyle('light-content', true);43 }4445 if (this.props.action === 'closeModal') {46 Animated.timing(this.state.scale, {47 toValue: 1,48 duration: 300,49 easing: Easing.in()50 }).start();51 Animated.spring(this.state.opacity, {52 toValue: 153 }).start();54 StatusBar.setBarStyle('dark-content', true);55 }56 };5758 render() {59 return (60 <RootView>61 <CustomModal />62 <AnimatedContainer63 style={{64 transform: [{ scale: this.state.scale }],65 opacity: this.state.opacity66 }}67 >68 <TouchableOpacity onPress={this.props.openModal}>69 <ButtonText>Open Modal</ButtonText>70 </TouchableOpacity>71 </AnimatedContainer>72 </RootView>73 );74 }75}7677const RootView = styled.View`78 flex: 1;79 background: black;80`;8182const Container = styled.View`83 flex: 1;84 background: white;85 border-top-left-radius: 10px;86 border-top-right-radius: 10px;87 justify-content: center;88 align-items: center;89`;9091const AnimatedContainer = Animated.createAnimatedComponent(Container);9293const ButtonText = styled.Text`94 font-size: 20px;95 font-weight: 600;96`;9798function mapStateToProps(state) {99 return { action: state.action };100}101102function mapDispatchToProps(dispatch) {103 return {104 openModal: () =>105 dispatch({106 type: 'OPEN_MODAL'107 })108 };109}110111export default connect(mapStateToProps, mapDispatchToProps)(HomeScreen);
In the above snippet, do take note of RootView
. We are also converting the good old Container
into an Animated view.
Conclusion
🔗This completes this tutorial about creating animated custom modal to provide a pleasant user experience in your react native application. You learned how to use the animated library and some of its methods such as spring
, timing
along with Easing
module. With the help of redux to manage state, you created a custom modal UI.
More Posts
Browse all posts