Let people pick photos and videos from their library or take new ones with the system camera
One.ImagePicker opens the system’s own screens for choosing media: the
photo picker for the library, and the camera app for a new photo or video.
People pick or capture there, and your app gets back files it can read,
upload, or show.
The library needs no permission on either platform, because the system picker runs outside your app and hands over only what people choose. The camera needs the camera permission, which the picker asks for the first time it opens. For a live preview inside your own layout, use Camera preview instead.
import { useState } from 'react'import { Button, Image, View } from 'react-native'import { One } from 'one'
function AvatarPicker() { const [uri, setUri] = useState<string | null>(null) return ( <View> {uri && <Image source={{ uri }} style={{ width: 96, height: 96, borderRadius: 48 }} />} <Button title="Choose photo" onPress={async () => { const result = await One.ImagePicker.launchLibrary({ allowsEditing: true, quality: 0.8 }) if (!result.canceled) setUri(result.assets[0].uri) }} /> </View> )}To use the camera, declare why in your app config before prebuild. The microphone line is needed only to record video on iOS:
import { one } from 'one/vite'
export default { plugins: [ one({ native: { app: { name: 'MyApp', imagePicker: { camera: 'Take a profile photo.', microphone: 'Record videos with sound.', }, }, }, }), ],}The library on iOS. launchLibrary presents PHPickerViewController
filtered to images, videos, or both, keeping the order people tapped. On
iOS 17 and later it asks for the most compatible version of each file, so
HEIC photos arrive as JPEG. The picker closes first, then One copies each
file, so a slow copy never looks like a frozen picker.
The library on Android. It opens the system photo picker. On devices
without it, One falls back to the documents picker filtered to the same
media types. A selectionLimit of 0 means the system maximum, which on
Android 13 and later is the platform’s own limit.
The camera. launchCamera opens UIImagePickerController on iOS and
the device’s camera app on Android. It takes a photo unless mediaTypes is
only 'videos', in which case it records one video. The first launch asks
for the camera permission. A denied permission, a device with no camera,
and backing out all resolve { canceled: true, assets: null }, so a
cancelled result does not tell you which happened; read
getCameraPermissions when it matters.
Copies, not originals. Every result is a copy in the app’s cache folder,
in a one-native-image-picker folder, returned as a file:// URI. You can
read, upload, or move it without any photo library access, and the
original stays untouched.
Cropping. allowsEditing shows a crop screen after one image is picked
or taken. iOS uses the system’s square crop. Android uses One’s own crop
screen, with a frame locked to aspect when you pass one.
Quality. quality below 1 re-encodes images as JPEG at that quality.
On iOS, PNG and BMP picks keep their pixels unchanged; Android re-encodes
them too, so a transparent PNG loses its transparency. Videos are never
re-encoded by the library picker; for the camera, videoQuality picks the
recording quality.
Web. Both calls open a hidden file input. launchCamera adds
capture="environment", which opens the camera on phones and a file dialog
on desktops. Results are blob URLs, and quality below 1 re-encodes through
a canvas. Permission calls always read granted.
| Function | Resolves |
|---|---|
| launchLibrary(options?) | The picked assets, or canceled |
| launchCamera(options?) | One captured asset, or canceled |
| getCameraPermissions() | The status, without asking |
| requestCameraPermissions() | The status, asking if undecided |
| ImagePickerOptions | Type | Description |
|---|---|---|
mediaTypesImagePickerMediaType | ImagePickerMediaType[] = "images"images, videos, or both. | ImagePickerMediaType | ImagePickerMediaType[] = "images" | images, videos, or both. |
selectionLimitnumber = 1The most assets to pick. 0 is the system maximum. The camera takes one. | number = 1 | The most assets to pick. 0 is the system maximum. The camera takes one. |
allowsEditingboolean = falseShow a crop screen. One image only. | boolean = false | Show a crop screen. One image only. |
aspect[number, number]Lock the Android crop frame to x by y. | [number, number] | Lock the Android crop frame to x by y. |
qualitynumber = 1JPEG quality from 0 to 1. Images only. | number = 1 | JPEG quality from 0 to 1. Images only. |
videoMaxDurationnumber = 0Longest camera video, in seconds. 0 is no limit. | number = 0 | Longest camera video, in seconds. 0 is no limit. |
videoQualityImagePickerVideoQuality = "high"high, medium, or low, for camera video. | ImagePickerVideoQuality = "high" | high, medium, or low, for camera video. |
| ImagePickerAsset | Type | Description |
|---|---|---|
uristringThe file URI of the copy. | string | The file URI of the copy. |
widthnumberWidth in pixels as displayed, or 0 if unknown. | number | Width in pixels as displayed, or 0 if unknown. |
heightnumberHeight in pixels as displayed, or 0 if unknown. | number | Height in pixels as displayed, or 0 if unknown. |
mimeTypestringThe file type, when known. | string | The file type, when known. |
fileNamestringThe original file name, when known. | string | The original file name, when known. |
fileSizenumberThe size of the copy in bytes. | number | The size of the copy in bytes. |
durationnumberVideo length in milliseconds. | number | Video length in milliseconds. |
typeImagePickerAssetTypeimage or video. | ImagePickerAssetType | image or video. |
A result is { canceled: false, assets } or { canceled: true, assets: null }.
A permission response has status (granted, denied, or undetermined),
granted, and canAskAgain.
| iOS | Android | Web | |
|---|---|---|---|
| Library | Photo picker | Photo picker | File dialog |
| Camera | System camera | Camera app | Phone camera |
| Crop shape | Square | Free or aspect | None |
| Camera video quality | High, medium, low | High or low | Browser’s |
| Results | Cache files | Cache files | Blob URLs |
E_IMAGE_PICKER_FAILED and the message another request is already in flight. Disable the button until the first call settles.imagePicker.camera,
launchCamera and requestCameraPermissions reject with a message that
names the setting. Camera video on iOS also rejects without
imagePicker.microphone, since the recording has sound.launchCamera
resolves canceled without prompting again. Call requestCameraPermissions
to ask, and send people to Settings when canAskAgain is false.aspect that is not two positive numbers, and a quality
outside 0 to 1 throw before any screen opens.allowsEditing and
aspect change nothing there.import { One } from 'one'
async function pickAndUpload(upload: (form: FormData) => Promise<void>) { const result = await One.ImagePicker.launchLibrary({ selectionLimit: 5, quality: 0.7 }) if (result.canceled) return 0 const form = new FormData() for (const asset of result.assets) { const blob = await (await fetch(asset.uri)).blob() form.append('photos', blob, asset.fileName ?? 'photo.jpg') } await upload(form) return result.assets.length}fetch reads the cache file on iOS and Android and the blob URL on web, so
one loop builds the upload on every platform.
import { Alert } from 'react-native'import { One } from 'one'
async function takePhoto() { const permission = await One.ImagePicker.requestCameraPermissions() if (!permission.granted) { if (!permission.canAskAgain) { Alert.alert('Camera is off', 'Turn on camera access in Settings.', [ { text: 'Not now', style: 'cancel' }, { text: 'Open Settings', onPress: () => One.openSettings() }, ]) } return null } const result = await One.ImagePicker.launchCamera({ quality: 0.8 }) return result.canceled ? null : result.assets[0]}Asking first separates a refusal from backing out of the camera, which
launchCamera alone reports the same way.
import { One } from 'one'
async function recordClip() { const result = await One.ImagePicker.launchCamera({ mediaTypes: 'videos', videoMaxDuration: 15, videoQuality: 'medium', }) if (result.canceled) return null const [clip] = result.assets return { uri: clip.uri, seconds: (clip.duration ?? 0) / 1000 }}The camera stops on its own at 15 seconds. duration is in milliseconds
and can be missing when the platform does not report it.
Edit this page on GitHub.