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