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