This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

Take a screenshot

Edit page

In this tutorial, learn how to capture a screenshot using a third-party library, save it with Expo Media Library on Android and iOS, and download it on the web.


In this chapter, we'll learn how to take a screenshot using a third-party library and save it on the device's media library. We'll use react-native-view-shot to take a screenshot and expo-media-library to save an image on device's media library. Web browsers don't give apps access to a media library, so on the web, we'll download the screenshot as an image file.

Watch: Taking screenshots in your universal Expo app
Watch: Taking screenshots in your universal Expo app

Capture a screenshot with react-native-view-shot and save it to the device's media library using expo-media-library.


1

Install libraries

To install react-native-view-shot and expo-media-library, run the following commands:

Terminal
- npx expo install react-native-view-shot expo-media-library
- yarn expo install react-native-view-shot expo-media-library
- pnpm expo install react-native-view-shot expo-media-library
- bun expo install react-native-view-shot expo-media-library

2

Prompt for permissions

An app that requires sensitive information, such as accessing a device's media library, has to prompt permission to allow or deny access. The usePermissions() hook from expo-media-library returns the permission permissionResponse and the requestPermission() method to ask for access. Our app only saves images to the media library, so we'll pass the writeOnly: true option to ask for write access without read access.

When the app loads for the first time and the permission status is neither granted nor denied, the value of the permissionResponse is null. When asked for permission, a user can either grant the permission or deny it. We can add a condition to check if it is not granted. If it is not granted, trigger the requestPermission() method. After getting the access, the value of permissionResponse.granted changes to true.

Add the following code snippet inside the src/app/(tabs)/index.tsx:

src/app/(tabs)/index.tsx
import { useEffect, useState } from 'react'; import * as MediaLibrary from 'expo-media-library'; // ...rest of the code remains same export default function Index() { const [permissionResponse, requestPermission] = MediaLibrary.usePermissions({ writeOnly: true }); // ...rest of the code remains same useEffect(() => { if (!permissionResponse?.granted) { requestPermission(); } }, []); // ...rest of the code remains same }

3

Create a ref to save the current view

We'll use react-native-view-shot to allow the user to take a screenshot within the app. This library captures the screenshot of a <View> as an image using the captureRef() method. It returns the URI of the captured screenshot image file.

In the previous chapter, we wrapped the <ImageViewer> and <EmojiSticker> components inside a <View>. We'll capture this <View>:

  1. Import captureRef from react-native-view-shot, and import useRef and the ComponentRef type from React.
  2. Create an imageRef reference variable to store the reference of the <View>.
  3. Pass the reference variable to the ref prop of the <View> that wraps the <ImageViewer> and <EmojiSticker> components.
src/app/(tabs)/index.tsx
import { useEffect, useRef, useState, type ComponentRef } from 'react'; import { captureRef } from 'react-native-view-shot'; export default function Index() { const imageRef = useRef<ComponentRef<typeof View>>(null); // ...rest of the code remains same return ( <SafeAreaView style={styles.container}> <View style={styles.imageContainer}> <View ref={imageRef} collapsable={false}> <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} /> {pickedEmoji && <EmojiSticker imageSize={40} stickerSource={pickedEmoji} />} </View> </View> {/* ...rest of the code remains same */} </SafeAreaView> ); }

In the above snippet, the collapsable prop is set to false. This allows the <View> component to screenshot only of the background image and emoji sticker.

4

Capture a screenshot and save it

We can capture a screenshot of the view by calling the captureRef() method from react-native-view-shot inside the onSaveImageAsync() function. It accepts an optional argument where we can pass the width and height of the screenshot capturing area. We can read more about available options in the library's documentation.

The captureRef() method also returns a promise that fulfills with the screenshot's URI. We will pass this URI as a parameter to MediaLibrary.Asset.create(), which saves the screenshot to the device's media library.

Inside src/app/(tabs)/index.tsx, update the onSaveImageAsync() function with the following code:

src/app/(tabs)/index.tsx
import { useEffect, useRef, useState, type ComponentRef } from 'react'; import { Platform, View, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import * as ImagePicker from 'expo-image-picker'; import * as MediaLibrary from 'expo-media-library'; import { captureRef } from 'react-native-view-shot'; import type { ImageSource } from 'expo-image'; import Button from '@/components/button'; import ImageViewer from '@/components/image-viewer'; import IconButton from '@/components/icon-button'; import CircleButton from '@/components/circle-button'; import EmojiPicker from '@/components/emoji-picker'; import EmojiList from '@/components/emoji-list'; import EmojiSticker from '@/components/emoji-sticker'; const PlaceholderImage = require('@/assets/images/background-image.png'); export default function Index() { const [selectedImage, setSelectedImage] = useState<string | undefined>(undefined); const [showAppOptions, setShowAppOptions] = useState<boolean>(false); const [isModalVisible, setIsModalVisible] = useState<boolean>(false); const [pickedEmoji, setPickedEmoji] = useState<ImageSource | undefined>(undefined); const [permissionResponse, requestPermission] = MediaLibrary.usePermissions({ writeOnly: true }); const imageRef = useRef<ComponentRef<typeof View>>(null); useEffect(() => { if (!permissionResponse?.granted) { requestPermission(); } }, []); const pickImageAsync = async () => { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], allowsEditing: true, quality: 1, }); if (!result.canceled) { setSelectedImage(result.assets[0].uri); setShowAppOptions(true); } else { alert('You did not select any image.'); } }; const onReset = () => { setShowAppOptions(false); }; const onAddSticker = () => { setIsModalVisible(true); }; const onModalClose = () => { setIsModalVisible(false); }; const onSaveImageAsync = async () => { try { const localUri = await captureRef(imageRef, { height: 440, quality: 1, }); await MediaLibrary.Asset.create(localUri); alert('Saved!'); } catch (e) { console.log(e); } }; return ( <SafeAreaView style={styles.container}> <View style={styles.imageContainer}> <View ref={imageRef} collapsable={false}> <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} /> {pickedEmoji && <EmojiSticker imageSize={40} stickerSource={pickedEmoji} />} </View> </View> {showAppOptions ? ( <View style={styles.footerContainer}> <View style={styles.optionsRow}> <IconButton icon={{ android: require('@expo/material-symbols/refresh.xml'), ios: 'arrow.counterclockwise', web: 'refresh', }} label="Reset" onPress={onReset} /> <CircleButton onPress={onAddSticker} /> <IconButton icon={{ android: require('@expo/material-symbols/download.xml'), ios: 'square.and.arrow.down', web: 'download', }} label="Save" onPress={onSaveImageAsync} /> </View> </View> ) : ( <View style={styles.footerContainer}> <Button theme="primary" label="Choose a photo" onPress={pickImageAsync} /> <Button label="Use this photo" onPress={() => setShowAppOptions(true)} /> </View> )} <EmojiPicker isVisible={isModalVisible} onClose={onModalClose}> <EmojiList onSelect={setPickedEmoji} onCloseModal={onModalClose} /> </EmojiPicker> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', alignItems: 'center', }, imageContainer: { flex: 1, paddingTop: Platform.select({ web: 96, default: 20 }), }, footerContainer: { flex: 1 / 3, alignItems: 'center', }, optionsRow: { alignItems: 'center', flexDirection: 'row', }, });

Now, choose a photo and add a sticker in the app. Then tap the Save button. We should see the following result on Android and iOS:

5

Save the screenshot on the web

react-native-view-shot can also capture a screenshot on the web. However, expo-media-library works only on Android and iOS. On the web, we'll download the screenshot as an image file instead.

Using Platform module from React Native, we can implement platform-specific behavior. On the web, captureRef() returns the screenshot as a data URL when we set the result option to 'data-uri'. We can pass this data URL to a link element to download the image.

Inside src/app/(tabs)/index.tsx, update the onSaveImageAsync() function to check whether the current platform is 'web' with the Platform.OS property:

  1. If it is not 'web', we'll keep using the same logic added for Android and iOS.
  2. If it is 'web', we'll use the captureRef() method to capture the current <View> as a JPEG image. Then, we'll create a link element with the data URL and click it to download the image.

The Platform module is already imported from react-native, because the imageContainer style uses it.

src/app/(tabs)/index.tsx
import { useEffect, useRef, useState, type ComponentRef } from 'react'; import { Platform, View, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import * as ImagePicker from 'expo-image-picker'; import * as MediaLibrary from 'expo-media-library'; import { captureRef } from 'react-native-view-shot'; import type { ImageSource } from 'expo-image'; import Button from '@/components/button'; import ImageViewer from '@/components/image-viewer'; import IconButton from '@/components/icon-button'; import CircleButton from '@/components/circle-button'; import EmojiPicker from '@/components/emoji-picker'; import EmojiList from '@/components/emoji-list'; import EmojiSticker from '@/components/emoji-sticker'; const PlaceholderImage = require('@/assets/images/background-image.png'); export default function Index() { const [selectedImage, setSelectedImage] = useState<string | undefined>(undefined); const [showAppOptions, setShowAppOptions] = useState<boolean>(false); const [isModalVisible, setIsModalVisible] = useState<boolean>(false); const [pickedEmoji, setPickedEmoji] = useState<ImageSource | undefined>(undefined); const [permissionResponse, requestPermission] = MediaLibrary.usePermissions({ writeOnly: true }); const imageRef = useRef<ComponentRef<typeof View>>(null); useEffect(() => { if (!permissionResponse?.granted) { requestPermission(); } }, []); const pickImageAsync = async () => { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], allowsEditing: true, quality: 1, }); if (!result.canceled) { setSelectedImage(result.assets[0].uri); setShowAppOptions(true); } else { alert('You did not select any image.'); } }; const onReset = () => { setShowAppOptions(false); }; const onAddSticker = () => { setIsModalVisible(true); }; const onModalClose = () => { setIsModalVisible(false); }; const onSaveImageAsync = async () => { if (Platform.OS !== 'web') { try { const localUri = await captureRef(imageRef, { height: 440, quality: 1, }); await MediaLibrary.Asset.create(localUri); alert('Saved!'); } catch (e) { console.log(e); } } else { try { const dataUrl = await captureRef(imageRef, { format: 'jpg', quality: 0.95, result: 'data-uri', }); const link = document.createElement('a'); link.download = 'sticker-smash.jpeg'; link.href = dataUrl; link.click(); } catch (e) { console.log(e); } } }; return ( <SafeAreaView style={styles.container}> <View style={styles.imageContainer}> <View ref={imageRef} collapsable={false}> <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} /> {pickedEmoji && <EmojiSticker imageSize={40} stickerSource={pickedEmoji} />} </View> </View> {showAppOptions ? ( <View style={styles.footerContainer}> <View style={styles.optionsRow}> <IconButton icon={{ android: require('@expo/material-symbols/refresh.xml'), ios: 'arrow.counterclockwise', web: 'refresh', }} label="Reset" onPress={onReset} /> <CircleButton onPress={onAddSticker} /> <IconButton icon={{ android: require('@expo/material-symbols/download.xml'), ios: 'square.and.arrow.down', web: 'download', }} label="Save" onPress={onSaveImageAsync} /> </View> </View> ) : ( <View style={styles.footerContainer}> <Button theme="primary" label="Choose a photo" onPress={pickImageAsync} /> <Button label="Use this photo" onPress={() => setShowAppOptions(true)} /> </View> )} <EmojiPicker isVisible={isModalVisible} onClose={onModalClose}> <EmojiList onSelect={setPickedEmoji} onCloseModal={onModalClose} /> </EmojiPicker> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', alignItems: 'center', }, imageContainer: { flex: 1, paddingTop: Platform.select({ web: 96, default: 20 }), }, footerContainer: { flex: 1 / 3, alignItems: 'center', }, optionsRow: { alignItems: 'center', flexDirection: 'row', }, });

On running the app in a web browser, we can now save a screenshot:

Summary

Chapter 7: Take a screenshot

We've successfully used react-native-view-shot and expo-media-library to capture a screenshot and save it on the device's library, and to download it on the web.

The app does everything we set out for it to do. In the next chapter, we will customize the app's status bar, splash screen, and app icon.

Next: Chapter 8: Configure status bar, splash screen and app icon