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