HomeGuidesReferenceLearn
ArchiveExpo SnackDiscord and ForumsNewsletter

Create a modal

In this tutorial, learn how to create a modal from React Native to select an image.


React Native provides a <Modal> component that presents content above the rest of your app. In general, modals are used to draw a user's attention toward critical information or guide them to take action. For example, in the second chapter, we used alert() to display a placeholder when a button is pressed. That's how a modal component displays an overlay.

In this chapter, we'll create a modal that shows an emoji picker list.

1

Declare a state variable to show buttons

Before implementing the modal, we are going to add three new buttons. These buttons will only be visible when the user picks an image from the media library or decides to use the placeholder image. One of these buttons will trigger the emoji picker modal.

Declare a state variable called showAppOptions in App.js. We'll use this variable to show or hide buttons that open the modal alongside a few other options.

This variable is a boolean. When the app screen loads, we'll set it to false so that the options are not shown before picking an image.

App.js
export default function App() {
  const [showAppOptions, setShowAppOptions] = useState(false);
  // ...rest of the code remains same
}

The value of this variable will be set to true when the user picks an image from the media library or decides to use the placeholder image.

Next, modify the pickImageAsync() function to set the value of showAppOptions to true after the user picks an image.

App.js
const pickImageAsync = async () => {
  // ...rest of the code remains same

  if (!result.canceled) {
    setSelectedImage(result.assets[0].uri);
    setShowAppOptions(true);

  } else {
    // ...rest of the code remains same
  }
};

Then, update the button with no theme by adding an onPress prop with the following value:

App.js
<Button label="Use this photo" onPress={() => setShowAppOptions(true)} />

Now, we can remove the alert on the <Button> component and update the onPress prop when rendering the second button in Button.js:

Button.js
<Pressable style={styles.button} onPress={onPress} >

Next, update App.js to conditionally render the <Button> component based on the value of showAppOptions. Also, move the buttons in the conditional operator block.

App.js
export default function App() {
  // ...
  return (
    <View style={styles.container}>
      {/* ...rest of the code remains same */}
      {showAppOptions ? (
        <View />
      ) : (
        <View style={styles.footerContainer}>
          <Button theme="primary" label="Choose a photo" onPress={pickImageAsync} />
          <Button label="Use this photo" onPress={() => setShowAppOptions(true)} />
        </View>
      )}
      <StatusBar style="auto" />
    </View>
  );
}

For now, when the value of showAppOptions is true, let's render an empty <View> component. We'll address this state in the next step.

2

Add buttons

Let's break down the layout of the option buttons we will implement in this chapter. The design looks like this:

It contains a parent <View> with three buttons aligned in a row. The button in the middle with the plus icon (+) will open the modal and is styled differently than the other two buttons.

Inside the components directory, create a new file called CircleButton.js with the following code:

CircleButton.js
import { View, Pressable, StyleSheet } from 'react-native';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';

export default function CircleButton({ onPress }) {
  return (
    <View style={styles.circleButtonContainer}>
      <Pressable style={styles.circleButton} onPress={onPress}>
        <MaterialIcons name="add" size={38} color="#25292e" />
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  circleButtonContainer: {
    width: 84,
    height: 84,
    marginHorizontal: 60,
    borderWidth: 4,
    borderColor: '#ffd33d',
    borderRadius: 42,
    padding: 3,
  },
  circleButton: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    borderRadius: 42,
    backgroundColor: '#fff',
  },
});

To render the plus icon, this button uses the <MaterialIcons> icon set from the @expo/vector-icons library.

The other two buttons also use <MaterialIcons> to display vertically aligned text labels and icons. Next, create a file named IconButton.js inside the components directory. This component accepts three props:

  • icon: the name that corresponds to the icon in the MaterialIcons library.
  • label: the text label displayed on the button.
  • onPress: the function called when the button is pressed.
IconButton.js
import { Pressable, StyleSheet, Text } from 'react-native';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';

export default function IconButton({ icon, label, onPress }) {
  return (
    <Pressable style={styles.iconButton} onPress={onPress}>
      <MaterialIcons name={icon} size={24} color="#fff" />
      <Text style={styles.iconButtonLabel}>{label}</Text>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  iconButton: {
    justifyContent: 'center',
    alignItems: 'center',
  },
  iconButtonLabel: {
    color: '#fff',
    marginTop: 12,
  },
});

Import these buttons into App.js and replace the empty <View> component from the previous step to display them. Let's also create the onPress functions for these buttons to add the functionality later.

Add Button options
// ... rest of the import statements
import CircleButton from './components/CircleButton';
import IconButton from './components/IconButton';

export default function App() {
  // ...rest of the code remains same
  const onReset = () => {
    setShowAppOptions(false);
  };

  const onAddSticker = () => {
    // we will implement this later
  };

  const onSaveImageAsync = async () => {
    // we will implement this later
  };

  return (
    <View style={styles.container}>
      {/* ...rest of the code remains same */}
      {showAppOptions ? (
        <View style={styles.optionsContainer}>
          <View style={styles.optionsRow}>
            <IconButton icon="refresh" label="Reset" onPress={onReset} />
            <CircleButton onPress={onAddSticker} />
            <IconButton icon="save-alt" label="Save" onPress={onSaveImageAsync} />
          </View>
        </View>
      ) : (
        // ...rest of the code remains same
      )}
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  // ...previous styles remain unchanged
  optionsContainer: {
    position: 'absolute',
    bottom: 80,
  },
  optionsRow: {
    alignItems: 'center',
    flexDirection: 'row',
  },
})

In the above snippet, the onReset() function is called when the user presses the reset button. When this button is pressed, we'll show the image picker button again.

Let's take a look at our app on Android, iOS and the web:

3

Create an emoji picker modal

The modal allows the user to choose an emoji from a list of available emoji. Create an EmojiPicker.js file inside the components directory. This component accepts three props:

  • isVisible: a boolean that determines whether the modal is visible or not.
  • onClose: a function that closes the modal.
  • children: used later to display a list of emoji.
EmojiPicker.js
import { Modal, View, Text, Pressable, StyleSheet } from 'react-native';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';

export default function EmojiPicker({ isVisible, children, onClose }) {
  return (
    <Modal animationType="slide" transparent={true} visible={isVisible}>
      <View style={styles.modalContent}>
        <View style={styles.titleContainer}>
          <Text style={styles.title}>Choose a sticker</Text>
          <Pressable onPress={onClose}>
            <MaterialIcons name="close" color="#fff" size={22} />
          </Pressable>
        </View>
        {children}
      </View>
    </Modal>
  );
}

Let's learn what the above code does.

  • The <Modal> component displays a title and a close button.
  • Its visible prop takes the value of isVisible and controls if the modal is open or closed.
  • Its transparent prop is a boolean value that determines whether the modal fills the entire view.
  • Its animationType prop determines how it enters and leaves the screen. In this case, it is sliding from the bottom of the screen.
  • Lastly, the <EmojiPicker> onClose prop is called when the user presses the close <Pressable>.

The next step is to add the corresponding styles for the <EmojiPicker> component:

EmojiPicker.js
const styles = StyleSheet.create({
  modalContent: {
    height: '25%',
    width: '100%',
    backgroundColor: '#25292e',
    borderTopRightRadius: 18,
    borderTopLeftRadius: 18,
    position: 'absolute',
    bottom: 0,
  },
  titleContainer: {
    height: '16%',
    backgroundColor: '#464C55',
    borderTopRightRadius: 10,
    borderTopLeftRadius: 10,
    paddingHorizontal: 20,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
  },
  title: {
    color: '#fff',
    fontSize: 16,
  },
  pickerContainer: {
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    paddingHorizontal: 50,
    paddingVertical: 20,
  },
});

Now, let's modify the App.js to:

  • Import the <EmojiPicker> component.
  • Then, create an isModalVisible state variable with the useState hook. It has a default value of false to ensure that the modal is hidden until the user presses the button to open it.
  • Replace the comment in the onAddSticker() function to update the isModalVisible variable to true when the user presses the button. This will open the emoji picker.
  • Create aonModalClose() function to update the isModalVisible state variable.
  • Place the <EmojiPicker> component at the bottom of the <App> component, above the <StatusBar> component.
Create a modal
// ...rest of the import statements remain same
import EmojiPicker from "./components/EmojiPicker";

export default function App() {
  const [isModalVisible, setIsModalVisible] = useState(false);
  // ...rest of the code remains same

  const onAddSticker = () => {
    setIsModalVisible(true);
  };

  const onModalClose = () => {
    setIsModalVisible(false);
  };

  return (
    <View style={styles.container}>
      {/* ...rest of the code remains same */}
      <EmojiPicker isVisible={isModalVisible} onClose={onModalClose}>
        {/* A list of emoji component will go here */}
      </EmojiPicker>
      <StatusBar style="auto" />
    </View>
  );
}

Here is the result after this step:

4

Display a list of emoji

Let's implement a horizontal list of emoji in the modal's content. We'll use the <FlatList> component from React Native for it.

Create a file named EmojiList.js file in the components directory and add the following code:

EmojiList.js
import { useState } from 'react';
import { StyleSheet, FlatList, Image, Platform, Pressable } from 'react-native';

export default function EmojiList({ onSelect, onCloseModal }) {
  const [emoji] = useState([
    require('../assets/images/emoji1.png'),
    require('../assets/images/emoji2.png'),
    require('../assets/images/emoji3.png'),
    require('../assets/images/emoji4.png'),
    require('../assets/images/emoji5.png'),
    require('../assets/images/emoji6.png'),
  ]);

  return (
    <FlatList
      horizontal
      showsHorizontalScrollIndicator={Platform.OS === 'web'}
      data={emoji}
      contentContainerStyle={styles.listContainer}
      renderItem={({ item, index }) => (
        <Pressable
          onPress={() => {
            onSelect(item);
            onCloseModal();
          }}>
          <Image source={item} key={index} style={styles.image} />
        </Pressable>
      )}
    />
  );
}

const styles = StyleSheet.create({
  listContainer: {
    borderTopRightRadius: 10,
    borderTopLeftRadius: 10,
    paddingHorizontal: 20,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
  },
  image: {
    width: 100,
    height: 100,
    marginRight: 20,
  },
});

The <FlatList> component above renders all the emoji images using a <Image> component wrapped with a <Pressable> component. Later, we will improve it so that the user can tap an emoji on the screen to make it appear as a sticker on the image.

The <FlatList> component takes an array of items, which in the above snippet is provided by the emoji array variable as the value of the data prop. Then, the renderItem prop takes the item from the data and returns the item in the list. Finally, we add <Image> and the <Pressable> components to display this item.

The horizontal prop renders the list horizontally instead of vertically. The showsHorizontalScrollIndicator checks the platform using Platform module from React Native and displays the horizontal scroll bar only on the web.

Now, modify the App component. Import the <EmojiList> component and replace the comments where the <EmojiPicker> component is used with the following code snippet:

App.js
//...rest of the import statements remain same
import EmojiList from './components/EmojiList';

// Inside App component to select the emoji from the list

export default function App() {
  const [pickedEmoji, setPickedEmoji] = useState(null);
  // ...rest of the code remain same

  return (
    <View style={styles.container}>
      {/* rest of the code remains unchanged */}
      <EmojiPicker isVisible={isModalVisible} onClose={onModalClose}>
        <EmojiList onSelect={setPickedEmoji} onCloseModal={onModalClose} />
      </EmojiPicker>
      <StatusBar style="auto" />
    </View>
  );
}

The onSelect prop on the <EmojiList> component selects the emoji and the onCloseModal prop closes the modal after emoji is selected.

Let's take a look at our app on Android, iOS and the web:

5

Display the selected emoji

Now we'll put the emoji sticker on the image.

Start by creating a new file in the components directory and call it EmojiSticker.js. Then, add the following code:

EmojiSticker.js
import { View, Image } from 'react-native';

export default function EmojiSticker({ imageSize, stickerSource }) {
  return (
    <View style={{ top: -350 }}>
      <Image
        source={stickerSource}
        resizeMode="contain"
        style={{ width: imageSize, height: imageSize }}
      />
    </View>
  );
}

This component receives two props:

  • imageSize: a value defined inside the <App> component. We will use this value in the next chapter to scale the image's size when tapped.
  • stickerSource: the source of the selected emoji image.

We'll import this component in the App.js file and update the <App> component to display the emoji sticker on the image conditionally. We'll do this by checking if the pickedEmoji state is not null.

Display selected emoji sticker
// ...rest of the import statements
import EmojiSticker from './components/EmojiSticker';

export default function App() {
  // ...rest of the code remains same

  return (
    <View>
      <View style={styles.imageContainer}>
        <ImageViewer placeholderImageSource={PlaceholderImage} selectedImage={selectedImage} />
        {pickedEmoji && <EmojiSticker imageSize={40} stickerSource={pickedEmoji} />}
      </View>
      {/* ...rest of the code remains same */}
    </View>
  );
}

Let's take a look at our app on Android, iOS and the web:

Next step

We successfully created the emoji picker modal and implemented the logic to select an emoji and display it over the image.

Add gestures

In the next chapter, let's add user interactions with gestures to drag the emoji and scale the size by tapping it.