Crop, rotate, and flip a single image. Standalone here, but built to be reused as the editing step
inside an upload flow rather than only living behind one — see
FileUpload's enableImageEditing prop.
<ImageEditor src="data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2296%22%20height%3D%2296%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22g%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23000%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23fff%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%2296%22%20height%3D%2296%22%20fill%3D%22url(%23g)%22%2F%3E%3Ccircle%20cx%3D%2230%22%20cy%3D%2266%22%20r%3D%2220%22%20fill%3D%22%23000%22%2F%3E%3Ccircle%20cx%3D%2270%22%20cy%3D%2230%22%20r%3D%2214%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E" />How it works
Rotate and flip are baked straight into a canvas — not applied as a CSS transform — the moment either changes, so the crop box always operates on a plain upright image in one consistent coordinate space regardless of the current rotation. The cost: changing rotation or flip resets the crop box, since there's no sound way to carry a crop rect across a coordinate space it wasn't drawn in. Rotate/flip first, then crop — the order most simple editors already expect.
The crop box is keyboard-operable: focus it and use the arrow keys to move it (hold Shift to move further per press), on top of the usual drag-to-move, drag-a-corner-to-resize interaction.
Examples
features turns individual tools off — useful when a consumer only wants, say, rotation without
exposing the full crop UI:
<ImageEditor src={photoUrl} features={{ crop: false }} />aspectRatios controls which presets the crop box can lock to — the first entry is selected by
default, and one without a ratio (like "Free") leaves it unconstrained:
<ImageEditor
src={photoUrl}
aspectRatios={[
{ label: "Square", ratio: 1 },
{ label: "Portrait", ratio: 3 / 4 },
{ label: "Free" },
]}
/>onSave receives the cropped result as both a Blob (for uploading) and a data URL (for an
immediate preview) — onCancel is only shown a Cancel button when provided, which is how the
toolbar knows whether it's being used as a modal step or embedded inline:
<ImageEditor
src={photoUrl}
onSave={({ blob, dataUrl }) => {
setPreview(dataUrl);
uploadFile(blob);
}}
onCancel={() => setEditing(false)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| aspectRatios | readonly | Free, 1:1, 4:3, 16:9 | — |
| className | text | — | — |
| features | readonly | `{ crop: true, rotate: true, flip: true }` | — |
| label | text | "Crop area" | Accessible label for the crop region. |
| onCancel | readonly | — | — |
| onSave | readonly | — | — |
| outputType | text | "image/png" | The saved result's MIME type. |
| src * | text | — | — |