Skip to content
Devix Open Source

Guide

Moving from Cropper.js or react-image-crop

From Cropper.js

// before (v1)
const cropper = new Cropper(image, { aspectRatio: 1, viewMode: 1 });
const canvas = cropper.getCroppedCanvas({ width: 512 });
canvas.toBlob((blob) => upload(blob), 'image/jpeg', 0.9);

// after
import { createImageCropper } from '@devix-labs/image-cropper';
import '@devix-labs/image-cropper/styles.css';

const picker = createImageCropper(input, { aspect: 1, maxWidth: 512, type: 'image/jpeg', quality: 0.9 });
upload(await picker.toBlob());
Cropper.js Here
new Cropper(imgElement, …) createImageCropper(fileInput, …) — it picks the picture too
aspectRatio aspect, which also takes [min, max]
viewMode: 1/2/3 mode: 'free' | 'fixed'
getCroppedCanvas().toBlob() toBlob(), toFile(), toDataURL()
getData() getCrop() — the same four numbers, in the original's pixels
setData() setCrop()
rotate(), scaleX(-1) rotate(), flip('x')
destroy() destroy(), which also gives the input back
cropper-* classes dxi-* classes, plus classNames for your own

If you are on v1 and have been putting off v2 — its most-reacted open issue is a request to keep supporting v1, with 35 votes, followed by two more about migrating and missing demos — this is a smaller move than that one, and it ends with a File rather than a canvas.

The one-pixel black seam along a crop's edge (#551, +18) does not happen here; there is a test that reads every pixel of the output's edges.

From react-image-crop

// before
const [crop, setCrop] = useState();
<ReactCrop crop={crop} onChange={setCrop} aspect={1} circularCrop>
  <img ref={imgRef} src={src} />
</ReactCrop>
// …then forty lines of canvas code to turn `crop` into a file,
//    and another bug when the displayed size is not the natural size

// after
import { ImageCropper } from '@devix-labs/image-cropper/react';
<ImageCropper ref={cropper} aspect={1} round mode="fixed" maxWidth={512} />
const file = await cropper.current.toFile();
react-image-crop Here
crop / onChange in percent or pixels of the displayed image getCrop() / onChange in pixels of the original — no scaling factor to get wrong
aspect aspect, which also takes a [min, max] range
circularCrop round — and the file is genuinely round, not just the preview
minWidth, maxWidth on the crop minWidth, maxWidth on the crop; maxWidth on the output is separate
your own canvas code toFile()
a wrapped <img> you must load yourself a file input that opens, drops and pastes

The four issues people keep opening — #380, #523, #566, #535 — are all the same missing function. It is toFile().

From react-easy-crop

// before
<Cropper image={src} crop={crop} zoom={zoom} aspect={1}
         onCropChange={setCrop} onCropComplete={(area, pixels) => setPixels(pixels)} />
// …then getCroppedImg(src, pixels) copied from the README

// after
<ImageCropper ref={cropper} aspect={1} mode="fixed" />
const file = await cropper.current.toFile();

croppedAreaPixels becomes getCrop(), and the getCroppedImg helper the README asks you to copy is toFile(). Blurriness when zoomed on Safari (#520) does not arise, because the output is drawn from the decoded original rather than from what is on screen.

From croppie

croppie was last published in 2020.

// before
const croppie = new Croppie(el, { viewport: { width: 200, height: 200, type: 'circle' } });
croppie.result({ type: 'blob', size: { width: 512, height: 512 } }).then(upload);

// after
const picker = createImageCropper(input, { mode: 'fixed', aspect: 1, round: true, width: 512, height: 512 });
upload(await picker.toBlob());

viewport + boundary become mode: 'fixed' with aspect and --dxi-stage-height; result() becomes toBlob().

Updated 15 Sep 2026