ImageEditor
Gesture-driven image crop editor with pan and pinch zoom, resizable crop box, circle stencil, rotate and flip, returning the edited image file.
Installation
pnpm dlx @docyrus/cli add @docyrus/rn-image-editorpnpm add react-native-gesture-handler react-native-reanimated react-native-svg expo-image-manipulator (optional) expo-image-picker (optional)expo-image-manipulator and expo-image-picker are optional peers (both work in Expo Go):
npx expo install expo-image-manipulator expo-image-picker- Without
expo-image-manipulatorthe editor still renders and shows a hint; Save returns the original image unchanged. - Without
expo-image-pickerthe Upload button is hidden.
Usage
import { ImageEditor, type ImageEditorResult } from '@/components/docyrus-native/image-editor';
<ImageEditor
src="https://example.com/photo.jpg"
aspectRatio={16 / 9}
onSave={(result: ImageEditorResult) => upload(result.uri)}
onCancel={() => navigation.goBack()}
/>Circle avatar crop
<ImageEditor
src={asset.uri}
stencilShape="circle"
size="sm"
variant="compact"
maxOutputSize={512}
saveFormat="jpeg"
compress={0.85}
onSave={result => setAvatar(result.uri)}
/>Crop limits and base64 output
minCropWidth / minCropHeight / maxCropWidth / maxCropHeight are source-image pixels (after rotation), like react-advanced-cropper on web. They clamp both the crop box and the zoom range.
<ImageEditor
src={uri}
minCropWidth={200}
minCropHeight={200}
maxCropWidth={1600}
maxCropHeight={1600}
base64
onSave={({ dataUrl }) => sendToApi(dataUrl)}
/>Replacing the image
When expo-image-picker is installed the toolbar shows an Upload button. The picked image replaces the one being edited (all edits reset), and onUpload receives the asset.
<ImageEditor
src={uri}
onUpload={asset => console.log(asset.fileName, asset.mimeType, asset.width, asset.height)}
onSave={handleSave}
/>Interactions
| Gesture / control | Effect |
|---|---|
| Drag the image | Pans the image under the crop box (clamped so the image always covers the box). |
| Pinch | Zooms around the focal point (range limited by the crop box and the min/max crop sizes). |
| Drag a corner handle | Resizes the crop box from that corner; the aspect ratio is kept when set. |
| Zoom in / Zoom out buttons | Zoom by 1.25× around the crop box centre. |
| Reset button | Clears rotation, flips, zoom and the crop box. |
| Rotate Left / Rotate Right | Rotates by 90°; the crop box resets to fit the rotated image. |
| Flip Horizontal / Flip Vertical | Mirrors the image in its current on-screen orientation. |
| Save | Rotate → flip → crop → optional resize via expo-image-manipulator, then onSave(result). |
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | — | Image URI (remote https:// or local file://). Changing it replaces the image and resets every edit. |
source | { uri: string } | — | Deprecated alias of src (kept for backward compatibility). src wins when both are set. |
onSave | (result: ImageEditorResult) => void | — | Receives the edited image. Without expo-image-manipulator it receives the original URI and size. |
onUpload | (asset: ImageEditorAsset) => void | — | Receives the replacement image picked from the library (requires expo-image-picker). |
onCancel | () => void | — | Renders a Cancel button when provided. |
onAction | (action: ImageEditorAction) => void | — | Fired for every toolbar / overlay action (analytics, side effects). |
disabled | boolean | false | Disables gestures, tools and Save. |
stencilShape | 'rectangle' | 'circle' | 'rectangle' | Crop stencil. circle draws a round mask (an ellipse when aspectRatio ≠ 1); the saved file is the bounding rectangle. |
aspectRatio | number | — | Crop width / height. Free when omitted; circle without a ratio uses 1. |
minCropWidth | number | — | Minimum crop width in source-image pixels. |
minCropHeight | number | — | Minimum crop height in source-image pixels. |
maxCropWidth | number | — | Maximum crop width in source-image pixels. |
maxCropHeight | number | — | Maximum crop height in source-image pixels. |
variant | 'default' | 'compact' | 'default' | compact uses a lighter frame and moves Save / Cancel into the toolbar as icons. |
size | 'sm' | 'default' | 'lg' | 'default' | Cropper height: 300 / 400 / 500 points. |
saveFormat | 'jpeg' | 'png' | 'webp' | 'png' | Output encoding (png matches web toDataURL()). |
compress | number | 1 | Compression 0–1 (1 = best quality). |
base64 | boolean | false | Also return base64 and a data: URL (web onSave(dataUrl) parity). |
maxOutputSize | number | — | Downscales the crop so its longest edge is at most this many pixels. |
className | string | — | Additional classes for the root. |
style | ViewStyle | — | Root style for dynamic values. |
ImageEditorResult
| Property | Type | Description |
|---|---|---|
uri | string | Local file URI of the edited image (the original URI when expo-image-manipulator is missing). |
width | number | Width in pixels. |
height | number | Height in pixels. |
base64 | string | undefined | Raw base64 payload, only when base64 is true. |
dataUrl | string | undefined | data:image/<format>;base64,…, only when base64 is true. |
ImageEditorAsset
| Property | Type | Description |
|---|---|---|
uri | string | Local file URI. |
fileName | string | File name (derived from the URI when the picker gives none). |
mimeType | string | MIME type (derived from the extension when missing, default image/jpeg). |
width | number | Width in pixels. |
height | number | Height in pixels. |
fileSize | number | undefined | Size in bytes, when known. |
ImageEditorAction
'rotate-left' | 'rotate-right' | 'flip-horizontal' | 'flip-vertical' | 'zoom-in' | 'zoom-out' | 'reset' | 'upload'
Helpers
The barrel also exports the optional-peer bridges, reused by AvatarSelect:
| Export | Signature | Description |
|---|---|---|
pickImageAsset | (options?: PickImageOptions) => Promise<ImageEditorAsset | null> | Opens the photo library (mediaTypes: ['images']). null when cancelled or expo-image-picker is missing. |
isImagePickerAvailable | () => boolean | Whether expo-image-picker is installed. |
isImageManipulatorAvailable | () => boolean | Whether expo-image-manipulator is installed. |
toImageEditorAsset | (asset) => ImageEditorAsset | Normalises a picker / manipulator result (fills fileName / mimeType). |
imageEditorVariants | tv() | Slot variants (base, cropper, toolbar, footer, toolButton, overlayButton). |
DEFAULT_ADJUSTMENTS | Adjustments | All-zero adjustments (web parity constant). |
PickImageOptions
| Property | Type | Default | Description |
|---|---|---|---|
allowsEditing | boolean | false | Show the OS crop UI. |
aspect | [number, number] | — | Android crop aspect (iOS always crops square with allowsEditing). |
quality | number | 1 | 0–1 JPEG quality. |
Differences from web
- Colour adjustments (brightness, saturation, contrast, hue — the web
EditorModevalues other thancrop) are web-only: React Native has no canvas filter pipeline. TheAdjustments/EditorModetypes are exported for shared code, but the native editor only crops. - Web
onSavereceives a data URL; native receives anImageEditorResultfile (setbase64for a data URL). - Web
onUploadreceives aFile; native receives anImageEditorAsset. - Rotate and flip are native-only tools.
i18n
Copy goes through useUiTranslation(): ui.common.save, ui.common.cancel, ui.imageEditor.uploadImage, ui.imageEditor.saveImage, ui.imageEditor.rotateLeft, ui.imageEditor.rotateRight, ui.imageEditor.flipHorizontal, ui.imageEditor.flipVertical, ui.imageEditor.zoomIn, ui.imageEditor.zoomOut, ui.imageEditor.reset, ui.imageEditor.noImage, ui.imageEditor.loadFailed, ui.imageEditor.manipulatorMissing, ui.imageEditor.saveFailed.
Type Exports
| Type | Description |
|---|---|
ImageEditorProps | Props for the ImageEditor component. |
ImageEditorResult | Edited image handed to onSave. |
ImageEditorAsset | Picked image handed to onUpload. |
ImageEditorAction | Toolbar / overlay action union. |
ImageEditorSaveFormat | 'jpeg' | 'png' | 'webp'. |
StencilShape | 'rectangle' | 'circle' (web parity). |
EditorMode | 'crop' | 'brightness' | 'saturation' | 'contrast' | 'hue' (web parity; only crop applies on native). |
Adjustments | { brightness, saturation, hue, contrast } (web parity; web-only feature). |
PickImageOptions | Options for pickImageAsset. |