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