components

ImageEditor

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.

tsx
Loading image…
<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" />
src

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:

tsx
<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:

tsx
<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:

tsx
<ImageEditor
  src={photoUrl}
  onSave={({ blob, dataUrl }) => {
    setPreview(dataUrl);
    uploadFile(blob);
  }}
  onCancel={() => setEditing(false)}
/>

Props

PropTypeDefaultDescription
aspectRatiosreadonlyFree, 1:1, 4:3, 16:9—
classNametext——
featuresreadonly`{ crop: true, rotate: true, flip: true }`—
labeltext"Crop area"Accessible label for the crop region.
onCancelreadonly——
onSavereadonly——
outputTypetext"image/png"The saved result's MIME type.
src *text——