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