All answers

Expo

How do I save an image to the photo library in an Expo app?

The photo library only takes a local file, so get the image into one first, then ask for photo library permission and call MediaLibrary.saveToLibraryAsync with the file's URI. For a remote image that means File.downloadFileAsync, and for a base64 string it means writing a file with the base64 encoding. PDFs can't go in the photo library.

Updated October 31, 2026

Saving an image to the phone looks like it should be one call, and it nearly is. The photo library only takes a file that’s already on the phone, so an image URL or a base64 string has to become a local file first. After that, the save itself is short.

How it works

expo-media-library is how an Expo app saves files to Photos on iPhone and the gallery on Android. Both saveToLibraryAsync and createAssetAsync take a local URI to an image or video file, the URI must include a file extension, and on Android it has to start with file:///. The difference is that createAssetAsync returns the new asset, which you need if you want to add it to an album, and saveToLibraryAsync doesn’t.

In SDK 55, expo-file-system is built around File, Directory, and Paths. The older function-based API, such as writeAsStringAsync, now lives at expo-file-system/legacy, so code from older tutorials needs that import path or the new classes.

How to save it

  1. Install both packages:

    npx expo install expo-media-library expo-file-system
  2. Add the permission text to app.json. On iPhone, saving fails without it, and you’ll need a new build after you add it:

    {
      "expo": {
        "plugins": [
          [
            "expo-media-library",
            { "savePhotosPermission": "Allow $(PRODUCT_NAME) to save photos you download." }
          ]
        ]
      }
    }
  3. Save an image from a URL by downloading it into the cache, then saving the file:

    import { File, Paths } from "expo-file-system";
    import * as MediaLibrary from "expo-media-library";
    
    export async function saveImageFromUrl(url: string) {
      const { granted } = await MediaLibrary.requestPermissionsAsync(true);
      if (!granted) return false;
    
      const file = await File.downloadFileAsync(url, new File(Paths.cache, "download.jpg"), {
        idempotent: true,
      });
      await MediaLibrary.saveToLibraryAsync(file.uri);
      return true;
    }

    Passing true to requestPermissionsAsync asks for write access only, which is all saving needs. idempotent: true lets the download overwrite the previous file instead of failing.

  4. Save a base64 image by writing it to a file with the base64 encoding. Strip the data:image/...;base64, prefix first if your string has one, and use the extension that matches the image:

    export async function saveBase64Image(base64: string) {
      const { granted } = await MediaLibrary.requestPermissionsAsync(true);
      if (!granted) return false;
    
      const data = base64.replace(/^data:image\/\w+;base64,/, "");
      const file = new File(Paths.cache, `image-${Date.now()}.png`);
      file.write(data, { encoding: "base64" });
      await MediaLibrary.saveToLibraryAsync(file.uri);
      return true;
    }

How you know it works

After you tap save and allow access, the image shows up at the top of Recents in Photos on iPhone, or in the gallery on Android, and it opens at full size.

If it doesn’t save

This URL does not contain a valid asset type means the file isn’t an image or video, which is what happens with a PDF, since the photo library only holds photos and videos. For a PDF, we’d open the share sheet instead, so the person can save it to Files or send it on:

import * as Sharing from "expo-sharing";

await Sharing.shareAsync(file.uri, { mimeType: "application/pdf", UTI: "com.adobe.pdf" });

Could not get the file's extension means the file name has no extension, so name it .jpg or .png. And if the error mentions NSPhotoLibraryAddUsageDescription, the permission text from step 2 isn’t in the build you’re running yet.

Ask the agent

If your app is in Draftbit, paste this into the agent, and it can read your code and make the change for you.

Add a Save to Photos button to my image screen. It should ask for permission, save the image to the phone's photo library whether we have a URL or a base64 string, and show a message when it's saved. Test it on an iPhone and an Android phone.

Open Draftbit

More Expo answers