Capacitor plugin that lets the user capture multiple photos and videos with a customizable UI overlay in a single camera session. Includes built-in image annotation powered by marker.js 2.
npm install camera-multi-capture@github:hemangsk/capacitor-camera-multicapture
npx cap syncAdd the following permissions to your iOS app's Info.plist file:
<key>NSCameraUsageDescription</key>
<string>This app needs access to camera to capture photos</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to photo library to save captured images</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app needs access to photo library to save captured images</string>The plugin automatically adds the necessary permissions to your Android app. However, if you need to declare them explicitly, add these to your android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />Before using the camera, check and request permissions:
import { CameraMultiCapture } from 'camera-multi-capture';
// Check current permissions
const permissions = await CameraMultiCapture.checkPermissions();
if (permissions.camera !== 'granted' || permissions.photos !== 'granted') {
// Request permissions
const result = await CameraMultiCapture.requestPermissions();
if (result.camera !== 'granted') {
console.error('Camera permission denied');
return;
}
}
// Now you can safely use the camera
const cameraResult = await CameraMultiCapture.start(options);https://github.com/hemangsk/capacitor-multi-preview-demo
import { initialize, CameraOverlayResult } from 'camera-multi-capture';
const result = await initialize({
containerId: 'camera-overlay',
quality: 90,
thumbnailStyle: {
width: '100px',
height: '100px'
},
/** more options */
}).then((result: CameraOverlayResult) => {
if (result.cancelled) {
console.log('User cancelled the camera overlay');
} else {
console.log('Captured images:', result.images);
}
// normally you'd want go back to previous screen
// this.navCtrl.pop();
// this.navCtrl.navigateBack();
});
// Single capture mode example - returns immediately after one photo
const singleResult = await initialize({
containerId: 'camera-overlay',
quality: 90,
maxCaptures: 1 // Automatically returns after capturing one image
}).then((result: CameraOverlayResult) => {
if (!result.cancelled && result.images.length > 0) {
console.log('Single image captured:', result.images[0]);
}
});<div id="camera-overlay"></div>#camera-overlay {
width: 100%; // or any other value for custom container
height: 100%;
background-color: transparent !important;
--background: transparent !important;
--ion-background-color: transparent !important;
}// Capture only one image and return immediately
const result = await initialize({
maxCaptures: 1, // Single capture mode
containerId: 'camera-container',
quality: 90
});The capture button supports dual gestures: a quick tap captures a photo, and a long-press starts video recording. During recording a pulsing indicator with an elapsed timer is shown, and the capture button glows red.
const result = await initialize({
containerId: 'camera-container',
quality: 90,
maxRecordingDuration: 30, // seconds; auto-stops when reached
});
// result.images – captured photos
// result.videos – captured videos (uri, thumbnail, duration)Videos appear as thumbnails with a play badge and duration label. Tapping a video thumbnail opens a fullscreen preview.
After capturing photos you can annotate them directly in the camera session. Tap a thumbnail to open the fullscreen preview, then tap the edit (pencil) icon to launch the marker.js 2 editor.
The editor supports freehand drawing, arrows, text, callouts, and more. Annotations are non-destructive — editor state is preserved so you can re-open and continue editing. On save the annotated image replaces the original in the gallery.
The plugin automatically detects available physical cameras (ultrawide, wide, telephoto) and displays appropriate zoom buttons. The system seamlessly switches between physical cameras when you select certain zoom levels.
// Semi-transparent buttons with shot counter
const result = await initialize({
containerId: 'camera-container',
quality: 90,
showShotCounter: true, // Display shot count
buttons: {
capture: {
style: {
backgroundColor: '#ffffff',
opacity: 0.8 // 80% opacity
}
},
done: {
style: {
backgroundColor: '#28a745',
opacity: 0.9 // 90% opacity
}
}
}
});// Enable flash auto mode
const result = await initialize({
containerId: 'camera-container',
flashAutoModeEnabled: true, // Allow auto flash mode
flash: 'auto' // Start with auto flash
});The overlay supports a JavaScript-based pinch-to-zoom gesture on both Android and iOS.
When enabled:
- Users can pinch in/out anywhere on the camera preview.
- The current zoom level is clamped to the device’s supported zoom range.
- Zoom buttons (e.g. 0.7x, 1x, 2x) stay in sync with the active zoom level.
- If
lockToNearestStepistrue, the zoom snaps to the nearest preset level when the pinch ends.
import { initialize, CameraOverlayResult } from "camera-multi-capture";
const result: CameraOverlayResult = await initialize({
containerId: "camera-container",
quality: 90,
// Enable JS pinch-to-zoom
pinchToZoom: {
enabled: true,
lockToNearestStep: true, // Snap to nearest preset (e.g. 0.7x, 1x, 2x)
},
buttons: {
// Optional: smart zoom buttons that are kept in sync with pinch
zoom: {
levels: [0.7, 1, 2, 3], // Example zoom steps; plugin may override with device-specific smart levels
style: {
radius: 30,
backgroundColor: "rgba(0,0,0,0.5)",
color: "#ffffff",
padding: "10px",
size: 24,
},
},
},
});Notes:
- Pinch handling is implemented entirely in the overlay layer (JavaScript), independent of native gesture recognizers.
- The plugin still uses native camera APIs (CameraX / AVFoundation) for the actual zoom, but all gesture detection runs on the web side.
- If
pinchToZoomis omitted orenabledisfalse, pinch gestures are ignored and only the zoom buttons are used.
// Enhanced button styling with shadows and filters
const result = await initialize({
containerId: 'camera-container',
quality: 90,
buttons: {
capture: {
style: {
backgroundColor: '#ffffff',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1)',
filter: 'brightness(1.1)'
}
},
done: {
style: {
backgroundColor: '#28a745',
boxShadow: '0 2px 8px rgba(40, 167, 69, 0.3)',
filter: 'saturate(1.2)'
}
},
cancel: {
style: {
backgroundColor: '#dc3545',
boxShadow: '0 2px 8px rgba(220, 53, 69, 0.3)',
filter: 'contrast(1.1)'
}
},
switchCamera: {
style: {
backgroundColor: 'rgba(0,0,0,0.5)',
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.3)',
filter: 'blur(0.5px) brightness(1.1)'
}
}
}
});Available Filter Effects:
blur(2px)- Blur effectbrightness(1.2)- Brightness adjustment (1.0 = normal)contrast(1.5)- Contrast enhancementgrayscale(100%)- Grayscale effectsaturate(1.3)- Saturation boosthue-rotate(90deg)- Hue rotationdrop-shadow(0 2px 4px rgba(0,0,0,0.2))- Drop shadow alternative- Multiple filters:
brightness(1.1) contrast(1.2) saturate(1.3)
Box Shadow Examples:
- Subtle:
0 2px 4px rgba(0, 0, 0, 0.1) - Prominent:
0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1) - Colored:
0 4px 12px rgba(59, 130, 246, 0.4) - Inset:
inset 0 1px 0 rgba(255, 255, 255, 0.2)
start(...)capture()stop()switchCamera()setZoom(...)updatePreviewRect(...)checkPermissions()requestPermissions()- Interfaces
- Type Aliases
start(options?: CameraOverlayOptions | undefined) => Promise<CameraOverlayResult>Starts the camera overlay session.
| Param | Type |
|---|---|
options |
CameraOverlayOptions |
Returns: Promise<CameraOverlayResult>
capture() => Promise<{ value: CameraImageData; }>Captures a single frame.
Returns: Promise<{ value: CameraImageData; }>
stop() => Promise<void>Stops and tears down the camera session.
switchCamera() => Promise<void>Switches the camera between front and back.
setZoom(options: { zoom: number; }) => Promise<void>Sets the zoom level of the camera.
| Param | Type |
|---|---|
options |
{ zoom: number; } |
updatePreviewRect(options: CameraPreviewRect) => Promise<void>Updates the camera preview rectangle dimensions. Call this when the container size changes (e.g., orientation change).
| Param | Type |
|---|---|
options |
CameraPreviewRect |
checkPermissions() => Promise<PermissionStatus>Check camera and photo library permissions
Returns: Promise<PermissionStatus>
requestPermissions() => Promise<PermissionStatus>Request camera and photo library permissions
Returns: Promise<PermissionStatus>
| Prop | Type |
|---|---|
images |
CameraImageData[] |
videos |
CameraVideoData[] |
cancelled |
boolean |
Permission status for the camera multi-capture plugin
| Prop | Type |
|---|---|
camera |
'granted' | 'denied' | 'prompt' |
photos |
'granted' | 'denied' | 'prompt' |
Structure for image data returned by the camera
| Prop | Type |
|---|---|
uri |
string |
base64 |
string |
| Prop | Type |
|---|---|
buttons |
CameraOverlayButtons |
thumbnailStyle |
{ width: string; height: string; } |
quality |
number |
containerId |
string |
previewRect |
CameraPreviewRect |
direction |
CameraDirection |
captureMode |
CaptureMode |
resolution |
Resolution |
zoom |
number |
autoFocus |
boolean |
maxCaptures |
number |
flashAutoModeEnabled |
boolean |
showShotCounter |
boolean |
pinchToZoom |
{ enabled?: boolean; lockToNearestStep?: boolean; } |
| Prop | Type |
|---|---|
capture |
{ icon?: string; style?: ButtonStyle; position?: 'center' | 'left' | 'right' | 'custom'; } |
done |
{ icon?: string; style?: ButtonStyle; text?: string; } |
cancel |
{ icon?: string; style?: ButtonStyle; text?: string; } |
switchCamera |
{ icon?: string; style?: ButtonStyle; position?: 'custom' | 'topLeft' | 'topRight'; } |
zoom |
{ icon?: string; style?: ButtonStyle; levels?: number[]; } |
| Prop | Type |
|---|---|
radius |
number |
color |
string |
backgroundColor |
string |
padding |
string |
size |
number |
activeColor |
string |
border |
string |
opacity |
number |
boxShadow |
string |
filter |
string |
| Prop | Type |
|---|---|
width |
number |
height |
number |
x |
number |
y |
number |
| Prop | Type |
|---|---|
width |
number |
height |
number |
Defines the style properties for camera buttons
OriginalButtonStyle
'front' | 'back'
'minimizeLatency' | 'maxQuality'
MIT
's comment at https://github.com/ionic-team/capacitor-plugins/issues/1616#issuecomment-1912900318
- Capawesome plugins repository for formatting scripts and documentation setup.
- marker.js 2 for the image annotation engine.
- bigger-picture for fullscreen media preview.