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