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