Docyrus

ImageEditor

Gesture-driven image crop editor with pan and pinch zoom, resizable crop box, circle stencil, rotate and flip, returning the edited image file.

iOSAndroid
Preview ImageEditor on your device

Scan with Expo Go

Download Expo Go, then scan the QR code to preview native components.

Installation

pnpm dlx @docyrus/cli add @docyrus/rn-image-editor
Required Packages(5 packages)
pnpm 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-manipulator the editor still renders and shows a hint; Save returns the original image unchanged.
  • Without expo-image-picker the 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 / controlEffect
Drag the imagePans the image under the crop box (clamped so the image always covers the box).
PinchZooms around the focal point (range limited by the crop box and the min/max crop sizes).
Drag a corner handleResizes the crop box from that corner; the aspect ratio is kept when set.
Zoom in / Zoom out buttonsZoom by 1.25× around the crop box centre.
Reset buttonClears rotation, flips, zoom and the crop box.
Rotate Left / Rotate RightRotates by 90°; the crop box resets to fit the rotated image.
Flip Horizontal / Flip VerticalMirrors the image in its current on-screen orientation.
SaveRotate → flip → crop → optional resize via expo-image-manipulator, then onSave(result).

API Reference

PropTypeDefaultDescription
srcstring—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).
disabledbooleanfalseDisables 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.
aspectRationumber—Crop width / height. Free when omitted; circle without a ratio uses 1.
minCropWidthnumber—Minimum crop width in source-image pixels.
minCropHeightnumber—Minimum crop height in source-image pixels.
maxCropWidthnumber—Maximum crop width in source-image pixels.
maxCropHeightnumber—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()).
compressnumber1Compression 0–1 (1 = best quality).
base64booleanfalseAlso return base64 and a data: URL (web onSave(dataUrl) parity).
maxOutputSizenumber—Downscales the crop so its longest edge is at most this many pixels.
classNamestring—Additional classes for the root.
styleViewStyle—Root style for dynamic values.

ImageEditorResult

PropertyTypeDescription
uristringLocal file URI of the edited image (the original URI when expo-image-manipulator is missing).
widthnumberWidth in pixels.
heightnumberHeight in pixels.
base64string | undefinedRaw base64 payload, only when base64 is true.
dataUrlstring | undefineddata:image/<format>;base64,…, only when base64 is true.

ImageEditorAsset

PropertyTypeDescription
uristringLocal file URI.
fileNamestringFile name (derived from the URI when the picker gives none).
mimeTypestringMIME type (derived from the extension when missing, default image/jpeg).
widthnumberWidth in pixels.
heightnumberHeight in pixels.
fileSizenumber | undefinedSize 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:

ExportSignatureDescription
pickImageAsset(options?: PickImageOptions) => Promise<ImageEditorAsset | null>Opens the photo library (mediaTypes: ['images']). null when cancelled or expo-image-picker is missing.
isImagePickerAvailable() => booleanWhether expo-image-picker is installed.
isImageManipulatorAvailable() => booleanWhether expo-image-manipulator is installed.
toImageEditorAsset(asset) => ImageEditorAssetNormalises a picker / manipulator result (fills fileName / mimeType).
imageEditorVariantstv()Slot variants (base, cropper, toolbar, footer, toolButton, overlayButton).
DEFAULT_ADJUSTMENTSAdjustmentsAll-zero adjustments (web parity constant).

PickImageOptions

PropertyTypeDefaultDescription
allowsEditingbooleanfalseShow the OS crop UI.
aspect[number, number]—Android crop aspect (iOS always crops square with allowsEditing).
qualitynumber10–1 JPEG quality.

Differences from web

  • Colour adjustments (brightness, saturation, contrast, hue — the web EditorMode values other than crop) are web-only: React Native has no canvas filter pipeline. The Adjustments / EditorMode types are exported for shared code, but the native editor only crops.
  • Web onSave receives a data URL; native receives an ImageEditorResult file (set base64 for a data URL).
  • Web onUpload receives a File; native receives an ImageEditorAsset.
  • 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

TypeDescription
ImageEditorPropsProps for the ImageEditor component.
ImageEditorResultEdited image handed to onSave.
ImageEditorAssetPicked image handed to onUpload.
ImageEditorActionToolbar / 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).
PickImageOptionsOptions for pickImageAsset.

On this page