xref: /expo/docs/pages/tutorial/create-a-modal.mdx (revision dfd15ebd)
1---
2title: Create a modal
3---
4
5import { SnackInline, Terminal } from '~/ui/components/Snippet';
6import Video from '~/components/plugins/Video';
7import { LinkBase } from '~/ui/components/Text';
8import ImageSpotlight from '~/components/plugins/ImageSpotlight';
9
10React Native provides a [`<Modal>` component](https://reactnative.dev/docs/modal) 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 <LinkBase href="/tutorial/build-a-screen/#step-7-enhance-the-reusable-button-component" openInNewTab>second chapter</LinkBase>, we used `alert()` to display a placeholder when a button is pressed. That's how a modal component displays an overlay.
11
12In this chapter, we'll create a modal that shows an emoji picker list.
13
14## Step 1: Declare a state variable to show buttons
15
16Before 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 picer modal.
17
18Declare 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.
19
20This 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.
21
22{/* prettier-ignore */}
23```jsx App.js
24export default function App() {
25  /* @info Create a state variable inside the App component. */const [showAppOptions, setShowAppOptions] = useState(false); /* @end */
26  // ...rest of the code remains same
27}
28```
29
30The 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.
31
32Next, modify the `pickImageAsync()` function to set the value of `showAppOptions` to `true` after the user picks an image.
33
34{/* prettier-ignore */}
35```jsx App.js
36const pickImageAsync = async () => {
37  // ...rest of the code remains same
38
39  if (!result.canceled) {
40    setSelectedImage(result.assets[0].uri);
41    /* @info After selecting the image, set the App options to true. */ setShowAppOptions(true); /* @end */
42
43  } else {
44    // ...rest of the code remains same
45  }
46};
47```
48
49Then, update the button with no theme by adding an `onPress` prop with the following value:
50
51{/* prettier-ignore */}
52```jsx App.js
53<Button label="Use this photo" /* @info Update the onPress prop.*/ onPress={() => setShowAppOptions(true)} /* @end */ />
54```
55
56Now, we can remove the `alert` on the `<Button>` component and update the `onPress` prop when rendering the second button in **Button.js**:
57
58{/* prettier-ignore */}
59```jsx Button.js
60<Pressable style={styles.button} /* @info Replace the alert() with the onPress.*/ onPress={onPress}/* @end */ >
61```
62
63Next, update **App.js** to conditionally render the `<Button>` component based on the value of `showAppOptions`. Also, move the buttons in the conditional operator block.
64
65{/* prettier-ignore */}
66```jsx App.js
67export default function App() {
68  // ...
69  return (
70    <View style={styles.container}>
71      /* ...rest of the code remains same */
72      /* @info Based on the value of showAppOptions, the buttons will be displayed. Also, move the existing buttons in the conditional operator block. */
73      {showAppOptions ? (
74        <View />
75      /* @end */
76      /* @info */) : ( /* @end */
77        <View style={styles.footerContainer}>
78          <Button theme="primary" label="Choose a photo" onPress={pickImageAsync} />
79          <Button label="Use this photo" onPress={() => setShowAppOptions(true)} />
80        </View>
81      /* @info */)}/* @end */
82      <StatusBar style="auto" />
83    </View>
84  );
85}
86```
87
88For now, when the value of `showAppOptions` is `true`, let's render an empty `<View>` component. We'll address this state in the next step.
89
90## Step 2: Add buttons
91
92Let's break down the layout of the option buttons we will implement in this chapter. The design looks like this:
93
94<ImageSpotlight
95  alt="Break down of the layout of the buttons row."
96  src="/static/images/tutorial/buttons-layout.jpg"
97  style={{ maxWidth: 480 }}
98  containerStyle={{ marginBottom: 10 }}
99/>
100
101It contains a parent `<View>` with three buttons aligned in a row. The button in the middle with the plus icon (+) will open the model and is styled differently than the other two buttons.
102
103Inside the **components** directory, create a new file called **CircleButton.js** with the following code:
104
105```jsx CircleButton.js
106import { View, Pressable, StyleSheet } from 'react-native';
107import MaterialIcons from '@expo/vector-icons/MaterialIcons';
108
109export default function CircleButton({ onPress }) {
110  return (
111    <View style={styles.circleButtonContainer}>
112      <Pressable style={styles.circleButton} onPress={onPress}>
113        <MaterialIcons name="add" size={38} color="#25292e" />
114      </Pressable>
115    </View>
116  );
117}
118
119const styles = StyleSheet.create({
120  circleButtonContainer: {
121    width: 84,
122    height: 84,
123    marginHorizontal: 60,
124    borderWidth: 4,
125    borderColor: '#ffd33d',
126    borderRadius: 42,
127    padding: 3,
128  },
129  circleButton: {
130    flex: 1,
131    justifyContent: 'center',
132    alignItems: 'center',
133    borderRadius: 42,
134    backgroundColor: '#fff',
135  },
136});
137```
138
139To render the plus icon, this button uses the `<MaterialIcons>` icon set from the `@expo/vector-icons` library.
140
141The 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:
142
143- `icon`: the name that corresponds to the icon in the `MaterialIcons` library.
144- `label`: the text label displayed on the button.
145- `onPress`: the function called when the button is pressed.
146
147```jsx IconButton.js
148import { Pressable, StyleSheet, Text } from 'react-native';
149import MaterialIcons from '@expo/vector-icons/MaterialIcons';
150
151export default function IconButton({ icon, label, onPress }) {
152  return (
153    <Pressable style={styles.iconButton} onPress={onPress}>
154      <MaterialIcons name={icon} size={24} color="#fff" />
155      <Text style={styles.iconButtonLabel}>{label}</Text>
156    </Pressable>
157  );
158}
159
160const styles = StyleSheet.create({
161  iconButton: {
162    justifyContent: 'center',
163    alignItems: 'center',
164  },
165  iconButtonLabel: {
166    color: '#fff',
167    marginTop: 12,
168  },
169});
170```
171
172Import 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.
173
174<SnackInline
175label="Add Button options"
176templateId="tutorial/03-button-options/App"
177dependencies={['expo-image-picker', '@expo/vector-icons/FontAwesome', '@expo/vector-icons', 'expo-status-bar', '@expo/vector-icons/MaterialIcons']}
178files={{
179  'assets/images/background-image.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/503001f14bb7b8fe48a4e318ad07e910',
180  'components/ImageViewer.js': 'tutorial/02-image-picker/ImageViewer.js',
181  'components/Button.js': 'tutorial/03-button-options/Button.js',
182  'components/CircleButton.js': 'tutorial/03-button-options/CircleButton.js',
183  'components/IconButton.js': 'tutorial/03-button-options/IconButton.js',
184}}>
185
186{/* prettier-ignore */}
187```jsx
188// ... rest of the import statements
189import CircleButton from './components/CircleButton';
190import IconButton from './components/IconButton';
191
192export default function App() {
193  // ...rest of the code remains same
194  /* @info Add placeholder functions that we will add logic for in the next sections. */
195  const onReset = () => {
196    setShowAppOptions(false);
197  };
198
199  const onAddSticker = () => {
200    // we will implement this later
201  };
202
203  const onSaveImageAsync = async () => {
204    // we will implement this later
205  };
206  /* @end */
207
208  return (
209    <View style={styles.container}>
210      /* ...rest of the code remains same */
211      {showAppOptions ? (
212        /* @info Replace empty View component with this snippet to display App option buttons. */
213        <View style={styles.optionsContainer}>
214          <View style={styles.optionsRow}>
215            <IconButton icon="refresh" label="Reset" onPress={onReset} />
216            <CircleButton onPress={onAddSticker} />
217            <IconButton icon="save-alt" label="Save" onPress={onSaveImageAsync} />
218          </View>
219        </View>
220        /* @end */
221      ) : (
222        /* ...rest of the code remains same */
223      )}
224      <StatusBar style="auto" />
225    </View>
226  );
227}
228
229const styles = StyleSheet.create({
230  // ...previous styles remain unchanged
231  /* @info Add styles for the new View components. */
232  optionsContainer: {
233    position: 'absolute',
234    bottom: 80,
235  },
236  optionsRow: {
237    alignItems: 'center',
238    flexDirection: 'row',
239  },
240  /* @end */
241})
242```
243
244</SnackInline>
245
246In 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.
247
248Let's take a look at our app on iOS, Android and the web:
249
250<ImageSpotlight
251  alt="Button options displayed after a image is selected."
252  src="/static/images/tutorial/button-options.jpg"
253  style={{ maxWidth: 720 }}
254  containerStyle={{ marginBottom: 0 }}
255/>
256
257## Step 3: Create an emoji picker modal
258
259The 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:
260
261- `isVisible`: a boolean that determines whether the modal is visible or not.
262- `onClose`: a function that closes the modal.
263- `children`: used later to display a list of emoji.
264
265{/* prettier-ignore */}
266```jsx EmojiPicker.js
267import { Modal, View, Text, Pressable, StyleSheet } from 'react-native';
268import MaterialIcons from '@expo/vector-icons/MaterialIcons';
269
270export default function EmojiPicker({ isVisible, children, onClose }) {
271  return (
272    <Modal animationType="slide" transparent={true} visible={isVisible}>
273      <View style={styles.modalContent}>
274        <View style={styles.titleContainer}>
275          <Text style={styles.title}>Choose a sticker</Text>
276          <Pressable onPress={onClose}>
277            <MaterialIcons name="close" color="#fff" size={22} />
278          </Pressable>
279        </View>
280        {children}
281      </View>
282    </Modal>
283  );
284}
285```
286
287Let's learn what the above code does.
288
289- The `<Modal>` component displays a title and a close button.
290- Its `visible` prop takes the value of `isVisible` and controls if the modal is open or closed.
291- Its `transparent` prop is a boolean value that determines whether the modal fills the entire view.
292- Its `animationType` prop determines how it enters and leaves the screen. In this case, it is sliding from the bottom of the screen.
293- Lastly, the `<EmojiPicker>` `onClose` prop is called when the user presses the close `<Pressable>`.
294
295The next step is to add the corresponding styles for the `<EmojiPicker>` component:
296
297{/* prettier-ignore */}
298```jsx EmojiPicker.js
299const styles = StyleSheet.create({
300  modalContent: {
301    height: '25%',
302    width: '100%',
303    backgroundColor: '#25292e',
304    borderTopRightRadius: 18,
305    borderTopLeftRadius: 18,
306    position: 'absolute',
307    bottom: 0,
308  },
309  titleContainer: {
310    height: '16%',
311    backgroundColor: '#464C55',
312    borderTopRightRadius: 10,
313    borderTopLeftRadius: 10,
314    paddingHorizontal: 20,
315    flexDirection: 'row',
316    alignItems: 'center',
317    justifyContent: 'space-between',
318  },
319  title: {
320    color: '#fff',
321    fontSize: 16,
322  },
323  pickerContainer: {
324    flexDirection: 'row',
325    justifyContent: 'center',
326    alignItems: 'center',
327    paddingHorizontal: 50,
328    paddingVertical: 20,
329  },
330});
331```
332
333Now, let's modify the **App.js** to:
334
335- Import the `<EmojiPicker>` component.
336- 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.
337- 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.
338- Create a`onModalClose()` function to update the `isModalVisible` state variable.
339- Place the `<EmojiPicker>` component at the bottom of the `<App>` component, below the `<StatusBar>` component.
340
341<SnackInline
342label="Create a modal"
343templateId="tutorial/04-modal/App"
344dependencies={['expo-image-picker', '@expo/vector-icons/FontAwesome', '@expo/vector-icons', 'expo-status-bar', '@expo/vector-icons/MaterialIcons']}
345files={{
346  'assets/images/background-image.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/503001f14bb7b8fe48a4e318ad07e910',
347  'components/ImageViewer.js': 'tutorial/02-image-picker/ImageViewer.js',
348  'components/Button.js': 'tutorial/03-button-options/Button.js',
349  'components/CircleButton.js': 'tutorial/03-button-options/CircleButton.js',
350  'components/IconButton.js': 'tutorial/03-button-options/IconButton.js',
351  'components/EmojiPicker.js': 'tutorial/04-modal/EmojiPicker.js',
352}}>
353
354{/* prettier-ignore */}
355```jsx
356// ...rest of the import statements remain same
357/* @info import the EmojiPicker component.*/ import EmojiPicker from "./components/EmojiPicker"; /* @end */
358
359export default function App() {
360  /* @info Create a state variable.*/const [isModalVisible, setIsModalVisible] = useState(false);  /* @end */
361  /* @hide const [showAppOptions, setShowAppOptions] = useState(false); */
362  const [showAppOptions, setShowAppOptions] = useState(false);
363  const [selectedImage, setSelectedImage] = useState(null);
364
365  const pickImageAsync = async () => {
366    let result = await ImagePicker.launchImageLibraryAsync({
367      allowsEditing: true,
368      aspect: [4, 3],
369      quality: 1,
370    });
371
372    if (!result.cancelled) {
373      setSelectedImage(result.uri);
374      setShowAppOptions(true);
375    } else {
376      alert("You did not select any image.");
377    }
378  };
379
380  const onReset = () => {
381    setShowAppOptions(false);
382  };
383  const onSaveImageAsync = async () => {
384    // we will implement this later
385  };
386  /* @end */
387
388  /* @info Update functions to control the modal's visibility.*/
389  const onAddSticker = () => {
390    setIsModalVisible(true);
391  };
392
393  const onModalClose = () => {
394    setIsModalVisible(false);
395  };
396  /* @end */
397
398  return (
399    <View style={styles.container}>
400      /* ...rest of the code remains same */
401      /* @info Render the EmojiPicker component at the bottom of the App component, just before the StatusBar. */
402      <EmojiPicker isVisible={isModalVisible} onClose={onModalClose}>
403        {/* A list of emoji component will go here */}
404      </EmojiPicker>
405      /* @end */
406      <StatusBar style="auto" />
407    </View>
408  );
409}
410```
411
412</SnackInline>
413
414Here is the result after this step:
415
416<ImageSpotlight
417  alt="A modal working on all platforms"
418  src="/static/images/tutorial/modal-creation.jpg"
419  style={{ maxWidth: 720 }}
420  containerStyle={{ marginBottom: 0 }}
421/>
422
423## Step 4: Display a list of emoji
424
425Let's implement a horizontal list of emoji in the modal's content. We'll use is the <LinkBase href="https://reactnative.dev/docs/flatlist" openInNewTab>`<FlatList>`</LinkBase> component from React Native for it.
426
427Create a file named **EmojiList.js** file in the **components** directory and add the following code:
428
429{/* prettier-ignore */}
430```jsx EmojiList.js
431import { useState } from 'react';
432import { StyleSheet, FlatList, Image, Platform, Pressable } from 'react-native';
433
434export default function EmojiList({ onSelect, onCloseModal }) {
435  const [emoji] = useState([
436    require('../assets/images/emoji1.png'),
437    require('../assets/images/emoji2.png'),
438    require('../assets/images/emoji3.png'),
439    require('../assets/images/emoji4.png'),
440    require('../assets/images/emoji5.png'),
441    require('../assets/images/emoji6.png'),
442  ]);
443
444  return (
445    <FlatList
446      horizontal
447      showsHorizontalScrollIndicator={Platform.OS === 'web' ? true : false}
448      data={emoji}
449      contentContainerStyle={styles.listContainer}
450      renderItem={({ item, index }) => {
451        return (
452          <Pressable
453            onPress={() => {
454              onSelect(item);
455              onCloseModal();
456            }}>
457            <Image source={item} key={index} style={styles.image} />
458          </Pressable>
459        );
460      }}
461    />
462  );
463}
464
465const styles = StyleSheet.create({
466  listContainer: {
467    borderTopRightRadius: 10,
468    borderTopLeftRadius: 10,
469    paddingHorizontal: 20,
470    flexDirection: 'row',
471    alignItems: 'center',
472    justifyContent: 'space-between',
473  },
474  image: {
475    width: 100,
476    height: 100,
477    marginRight: 20,
478  },
479});
480```
481
482The `<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.
483
484The `<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.
485
486The `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.
487
488Now, modify the `App` component. Import the `<EmojiList>` component and replace the comments where the `<EmojiPicker>` component is used with the following code snippet:
489
490{/* prettier-ignore */}
491```jsx App.js
492//...rest of the import statements remain same
493/* @info Import the EmojiList component.*/ import EmojiList from './components/EmojiList';/* @end */
494
495// Inside App component to select the emoji from the list
496
497export default function App() {
498  /* @info Define a state variable. */const [pickedEmoji, setPickedEmoji] = useState(null);/* @end */
499  // ...rest of the code remain same
500
501  return (
502    <View style={styles.container}>
503      /* rest of the code remains unchanged */
504      <EmojiPicker modalVisible={modalVisible} onClose={onModalClose}>
505        /* @info Render the EmojiList component inside the EmojiPicker component. */
506        <EmojiList onSelect={setPickedEmoji} onCloseModal={onModalClose} />
507        /* @end */
508      </EmojiPicker>
509      <StatusBar style="auto" />
510    </View>
511  );
512  )
513}
514```
515
516The `onSelect` prop on the `<EmojiList>` component selects the emoji and the `onCloseModal` prop closes the modal after emoji is selected.
517
518Let's take a look at our app on iOS, Android and the web:
519
520<Video file="tutorial/emoji-picker.mp4" />
521
522## Step 5: Display the selected emoji
523
524Now we'll put the emoji sticker on the image.
525
526Start by creating a new file in the **components** directory and call it **EmojiSticker.js**. Then, add the following code:
527
528```jsx EmojiSticker.js
529import { View, Image } from 'react-native';
530
531export default function EmojiSticker({ imageSize, stickerSource }) {
532  return (
533    <View style={{ top: -350 }}>
534      <Image
535        source={stickerSource}
536        resizeMode="contain"
537        style={{ width: imageSize, height: imageSize }}
538      />
539    </View>
540  );
541}
542```
543
544This component receives two props:
545
546- `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.
547- `stickerSource`: the source of the selected emoji image.
548
549We'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`.
550
551<SnackInline
552label="Display selected emoji sticker"
553templateId="tutorial/05-emoji-list/App"
554dependencies={['expo-image-picker', '@expo/vector-icons/FontAwesome', '@expo/vector-icons', 'expo-status-bar', '@expo/vector-icons/MaterialIcons']}
555files={{
556  'assets/images/background-image.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/503001f14bb7b8fe48a4e318ad07e910',
557  'assets/images/emoji1.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/be9751678c0b3f9c6bf55f60de815d30',
558  'assets/images/emoji2.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/7c0d14b79e134d528c5e0801699d6ccf',
559  'assets/images/emoji3.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/d713e2de164764c2ab3db0ab4e40c577',
560  'assets/images/emoji4.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/ac2163b98a973cb50bfb716cc4438f9a',
561  'assets/images/emoji5.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/9cc0e2ff664bae3af766b9750331c3ad',
562  'assets/images/emoji6.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/ce614cf0928157b3f7daa3cb8e7bd486',
563  'components/ImageViewer.js': 'tutorial/02-image-picker/ImageViewer.js',
564  'components/Button.js': 'tutorial/03-button-options/Button.js',
565  'components/CircleButton.js': 'tutorial/03-button-options/CircleButton.js',
566  'components/IconButton.js': 'tutorial/03-button-options/IconButton.js',
567  'components/EmojiPicker.js': 'tutorial/04-modal/EmojiPicker.js',
568  'components/EmojiList.js': 'tutorial/05-emoji-list/EmojiList.js',
569  'components/EmojiSticker.js': 'tutorial/05-emoji-list/EmojiSticker.js',
570}}>
571
572{/* prettier-ignore */}
573```jsx
574// ...rest of the import statements
575import EmojiSticker from './components/EmojiSticker';
576
577export default function App() {
578  // ...rest of the code remains same
579
580  return (
581    <View>
582      <View style={styles.imageContainer}>
583        <ImageViewer placeholderImageSource={PlaceholderImage} selectedImage={selectedImage} />
584        /* @info */{pickedEmoji !== null ? <EmojiSticker imageSize={40} stickerSource={pickedEmoji} /> : null} /* @end */
585      </View>
586      /* ...rest of the code remains same*/
587    </View>
588  );
589}
590```
591
592</SnackInline>
593
594Let's take a look at our app on iOS, Android and the web:
595
596<Video file="tutorial/select-emoji.mp4" />
597
598## Up next
599
600We successfully created the emoji picker modal and implemented the logic to select an emoji and display it over the image.
601
602In the next chapter, let's add [user interactions with gestures](/tutorial/gestures) to drag the emoji and scale the size by tapping it.
603