Taking Photos in a Capacitor App: Camera Plugin Setup, Capture Code, and the Mistakes That Crash It
Set up the Capacitor Camera plugin correctly: Android and iOS permission declarations, capture code with permission checks, and why resultType Uri beats base64 for full-resolution photos.
28 Sept 2025, 09:51 UTC

If your Capacitor app needs to capture a photo from the native camera UI on Android and iOS, the official @capacitor/camera plugin is the supported path. The short version: install the plugin, declare one permission on Android and one usage string on iOS, then call Camera.getPhoto() and handle the result as either a base64 string or a file URI. The parts that actually break in practice are the permission declarations and the memory cost of large base64 results — both covered below.
Install and sync
Run these from your project's root directory (where package.json lives), with whatever user owns the project — no elevated permissions needed:
npm install @capacitor/camera
npx cap synccap sync copies your web build into the native projects and updates the native dependency lists (Gradle for Android, CocoaPods/SPM for iOS). If you skip it, the JavaScript side loads but the native bridge has no plugin registered, and calls fail at runtime.
Declare permissions before you ever call the camera
Both platforms refuse camera access without a declared reason, and they fail differently.
Android
Open android/app/src/main/AndroidManifest.xml and add inside the <manifest> element:
<uses-permission android:name="android.permission.CAMERA" />Missing this entry causes a runtime failure when the camera intent launches — not a build error — so it slips through CI easily. On modern Android versions the plugin prompts the user at runtime, but the manifest declaration is still required for the prompt to work.
iOS
Open ios/App/App/Info.plist and add:
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to take photos.</string>Without this key, iOS terminates the app the first time the camera is requested. If you also save captured photos to the user's photo library, add NSPhotoLibraryAddUsageDescription as well — omitting it makes the save silently fail after the capture appears to succeed.
Capture a photo
In your web code (any framework — this is plain TypeScript):
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
async function takePhoto() {
const perm = await Camera.checkPermissions();
if (perm.camera !== 'granted') {
const req = await Camera.requestPermissions();
if (req.camera !== 'granted') return; // user declined; show guidance
}
const photo = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
});
// photo.webPath is displayable directly in an <img> tag
// photo.path is the native file path for uploads
return photo;
}Checking permissions first lets you show your own explanation UI before the system prompt, which matters because a user who denies the bare system dialog often can't be prompted again without visiting Settings.
Base64 vs. URI: the decision that affects memory
resultType is the choice with real consequences:
| Option | You get | Good for | Risk |
|---|---|---|---|
CameraResultType.Base64 | base64String in JS memory | Small images, immediate canvas/<img> use | Large strings can pressure the JS heap; base64 inflates size ~33% |
CameraResultType.Uri | File path + webPath | Full-resolution photos, uploads | You must handle the file lifecycle yourself |
CameraResultType.DataUrl | Ready-to-use data URL | Direct <img> binding | Same memory cost as base64 |
A 12-megapixel photo at quality 90 can produce a base64 string of several megabytes. On lower-end Android devices, holding a few of those in memory — especially while also rendering them — can trigger garbage-collection stalls or out-of-memory kills. The safer default for anything beyond thumbnails is Uri: display with webPath, and read the file via the Filesystem plugin only when you need the bytes for upload.
Limits worth knowing
- Android preview and capture resolution are capped by what the device's camera hardware reports; you can't request arbitrary sizes through this plugin.
- iOS restricts camera use while the app is backgrounded — capture calls must happen while the app is in the foreground.
qualityonly applies to JPEG output and trades file size against artifacts; it does nothing for PNG results.- Orientation is a common surprise: photos taken in landscape may need EXIF-aware display. The plugin handles most of this, but test on a real device held sideways, not just the emulator.
Common mistakes
- Forgetting
npx cap syncafter installing — the plugin exists in JS but not natively. - Declaring the iOS key in the wrong plist — it must be the app's
Info.plist, not a framework's. - Using
resultType: 'uri'and then treating the path as a web URL — usewebPathfor display,pathfor native file access. - Not handling denial —
getPhoto()rejects when the user cancels or denies; an unhandled rejection looks like a crash in logs. - Requesting base64 for every photo when a URI would do, causing memory pressure on older devices.
Verify the setup
Three quick checks before shipping:
- Run
npx cap open androidand confirm the manifest contains the CAMERA permission. - Run
npx cap open iosand confirmNSCameraUsageDescriptionappears in Info.plist. - On a real device, call
Camera.checkPermissions()and log the result — it should returngrantedafter the user accepts, and your capture should resolve with a non-emptywebPathorbase64String. Emulators often have no camera or a fake one, so a physical device is the meaningful test.
Note: API details here reflect the Capacitor 5/6-era plugin. Check the plugin's README for your installed major version, since option names have changed between releases.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.