zriz vs react-easy-crop
react-easy-crop is one React component that moves a photo or video under a fixed frame and reports the crop as numbers. zriz is a set of unstyled parts with a resizable crop box, a straighten dial and a file export.
We make zriz, so read this page with that in mind. It compares react-easy-crop 6.2.5 with @zriz/react 0.2.1. If something here is wrong, open an issue and we will fix it.
To move a project over, paste this into Claude Code, Cursor or another coding agent:
Migrate this project from react-easy-crop to zriz. Read https://zriz.dev/docs/zriz-vs-react-easy-crop.md and follow its section on migrating. Keep the crop behaviour the same where zriz can, and list for me anything zriz has no equivalent for before you remove it.
In short
react-easy-crop describes itself in its README as "A React component to crop images/videos with easy interactions". It is one styled component. The photo or video moves under a fixed frame, and the component reports the crop as numbers. Your app holds the state and writes the code that makes the file. Wheel and pinch zoom are built in, and a zoom slider is the app's to draw.
Pick react-easy-crop when you crop video, when the project runs React 16.4 to 18.1 or needs a CommonJS build, or when a fixed frame with pan and zoom is all the screen needs. Setup is one component. Its basic example adds about 7 kB to the bundle, against 22 to 30 kB for zriz. It has been on npm since 2018 and has over 5 million downloads a week. It also allows a crop that shows area outside the photo, takes any angle in one rotation prop and shows a remote image from a server without CORS headers. zriz does none of these.
Pick zriz when the user resizes the crop with handles or picks a free ratio, when a tilted crop must hold no empty corner in the saved value and the file, or when you want the library to make the file, a round PNG included. Its zoom slider, straighten dial, rotate and flip buttons come as parts with roles and keys. zriz is new and little used. Its first release was in November 2026, it has one maintainer and under a thousand downloads a week.
Numbers
| Fact | react-easy-crop | zriz |
|---|---|---|
| First release | 2018 | November 2026 |
| npm downloads a week | 5.5M+ | Under 1,000 |
| GitHub stars | 2.7k+ | Under 100 |
| Licence | MIT | MIT |
| Dependencies | One, normalize-wheel | None |
| React | 16.4 and newer | 18.2 and 19 |
| Size | About 7 kB | 22 to 30 kB |
Size is minified JavaScript and CSS, compressed with Brotli, without React. For react-easy-crop it is the basic example from its docs, built with esbuild. That number includes normalize-wheel and the component's CSS, which ships as a string inside the JavaScript. For zriz it depends on the parts you import: about 22 kB for Root, Viewport, Image, Mask and Area, and about 29 kB with every part. Up to 11 kB more loads later, for reading the file, export, sharpening and SVG. We measured the zriz sets with Rolldown. Built with esbuild as one file, the way the other example was, zriz comes to about 33 kB for those five parts and about 41 kB with every part, because the code that loads later is then in that file. Bundle size has each set. @zriz/react needs @zriz/core and React as peers.
Features
| Feature | react-easy-crop | zriz |
|---|---|---|
| Crop box with handles | NoThe frame is fixed | YesFour corners and four edges |
| Photo moves under the frame | Yes | Yes |
| Aspect ratio | PartlyAny number, no free ratio | YesFree, original or any ratio |
| Round crop | PartlyRound mask, the output stays a rectangle | YesMask and export |
| Zoom | YesWheel, pinch and the zoom prop | YesPinch, Ctrl + wheel, slider part, keys |
| Straighten (free rotation) | YesAny angle, the app builds the control | Partly45° each way plus quarter turns |
| Crop stays inside the photo at an angle | NoThe frame can show empty area | YesThe crop shrinks to fit |
| Quarter turns and flip | PartlyTurns through rotation, no flip in output | YesAnimated |
| Touch gestures | YesPan, pinch, twist with onRotationChange | YesPan, pinch, twist, two handles at once |
| Keyboard | PartlyArrow keys move the photo | YesCrop area, dial, zoom and ratio group; handles no |
| Screen reader | NoIt sets no role or ARIA attribute | PartlyRoles and live region; you write the labels |
| Image export | NoCrop numbers and a docs function | YesJPEG, PNG or WebP blob |
| Saved crop | YesPercentages, the app stores the angle | YesFractions of the photo, restores exactly |
| Ready-made UI | PartlyStyled frame, mask and grid, no controls | NoUnstyled parts and an optional stylesheet |
In zriz the crop area, the dial, the zoom slider and the ratio group take keys, and the handles do not. Alt + arrows on the crop area resize it about its centre.
An app can give react-easy-crop a role and a label through cropperProps and an alt through mediaProps. It crops video too, which this table has no row for.
What you will notice
Setup is shorter in react-easy-crop. One component with image, crop, zoom and their change handlers fills a parent that has a size and position: relative. zriz takes five parts for the same screen and seven with a zoom slider. Viewport needs a height, Zoom needs a width and a height, and you write every label.
A tilted crop behaves differently. react-easy-crop sizes the frame from the bounding box of the rotated photo, see helpers.ts, so the frame can reach past the photo. At 15° and zoom 1 in Chromium, with a 4:3 frame on a 3:2 photo in a 480 by 480 px container, 15% of the frame showed the background, and the reported crop was 2400 by 1800 px from a 2400 by 1600 px photo. Another container shape gives other numbers. See issue 350. zriz shrinks the crop about its centre until all four corners sit on the photo, so no corner is empty. In the same run, zriz at 15° shrank its full-photo crop to 74% of each side and showed no empty area. That holds for the saved value and the exported file. While a finger pulls the photo past an edge, the background shows until release.
The wheel does different things. In react-easy-crop every wheel event with a vertical delta zooms by default, with or without Ctrl. zoomWithScroll={false} turns that off, and onWheelRequest lets the app refuse one event. In zriz a plain wheel pans the photo and Ctrl + wheel zooms. wheel="zoom" on Viewport makes a plain wheel zoom in zriz too.
Keyboard and screen reader support differ. react-easy-crop has one tab stop, the crop area. Arrow keys move the photo by 1 CSS pixel by default, set with keyboardStep, Shift makes the step 5 times smaller, and no key zooms or rotates. The crop area has no role and no name unless the app adds them. In zriz the crop area is role="application" with keys for move, resize, zoom and reset, and the dial and zoom parts are sliders. zriz ships no words, so the app writes each aria-label. axe reported no findings on either demo in Chromium.
react-easy-crop hands you two rectangles, in percent and in pixels, and its docs give a getCroppedImg canvas function to copy into the app. The angle and the zoom are not in that data, so the app stores rotation next to it. onCropComplete also fires after the photo loads and on each change of zoom or rotation, see Cropper.tsx. zriz has exportCrop, which returns a JPEG, PNG or WebP blob, and one value that holds position, size, angle, quarter turns and flip. The export runs in a worker by default and falls back to the main thread where workers or OffscreenCanvas are missing or blocked.
Migrate from react-easy-crop
zriz has no equivalent for the video prop, for a crop that shows area outside the photo, for cropSize, objectFit, zoomSpeed, onCropSizeChange or roundCropAreaPixels. A crop outside the photo means restrictPosition={false}, a minZoom below 1, or a tilted crop with empty corners. zriz also needs React 18.2 or 19 and ships no CommonJS build. It fetches a URL in CORS mode, so a remote image from a server without CORS headers shows in react-easy-crop and fails to load in zriz. If the project depends on one of these, stop here.
<Cropper />RootwithViewport,Image,MaskandAreainsideimagesrconRoot, a URL,FileorBlobvideoNo equivalentcrop,onCropChangeNo prop. The position isvalue.center, andpanBy()moves itzoom,onZoomChangezoomonRootwithonValueChange, or theZoompartminZoomNo equivalent. Zoom starts at 1, the largest crop that fitsmaxZoom, default 3maxZoomonRoot, default 15rotation,onRotationChangevalue.rotationup to 45° each way plusvalue.quarterTurns, set byDialandRotateaspectaspectonRoot, a number,"free"or"original"cropShape="round"shape="circle", which also rounds the exportcropSize,objectFitNo equivalentzoomSpeed,onCropSizeChange,roundCropAreaPixelsNo equivalentrestrictPosition={false}No equivalentshowGridTheGridpart, shown during gestureszoomWithScrollwheelonViewport, one of"pan","zoom"or"none"onWheelRequestwheelonViewport.wheel="none"leaves a plain wheel to the page and keeps Ctrl + wheel for zoom.details.cancel()inonValueChangerejects a change and does not scroll the pageonTouchRequestdetails.cancel()inonValueChangerejects the change. It does not hand the touch back to the page, so a finger on the cropper never scrolls itonInteractionStart,onInteractionEndgesturestartandgestureendevents on the cropper, ordata-interactingonRootonCropCompleteonValueCommit, with the pixel plan indetails.pixelCrop. It does not fire for the first photo until the user changes the croponCropAreaChangeonValueChangeonMediaLoadedonLoadinitialCroppedAreaPercentages,initialCroppedAreaPixelsdefaultValueorvalue, after the conversion belowgetInitialCropFromCroppedAreaPercentages,getInitialCropFromCroppedAreaPixelsNot needed. A zriz value restores itselfsetImageRef,setVideoRef,setCropperRefArefon the parttransformused for a flipTheFlippart andvalue.mirroredclasses,styleclassNameandstyleon each partmediaPropsProps onImagecropperPropsProps onArea, such asaria-labeldisableAutomaticStylesInjection,nonceNot needed. zriz injects nostyletag, and its stylesheet is an optional importkeyboardStepNo prop. Arrows move the crop by 1% of its size, 10% with ShiftgetCroppedImgfrom the docsexportCrop(cropper, options)from@zriz/core/dom
zriz has no importer for saved react-easy-crop data, so convert it once. A saved zoom or crop does not carry over, because the scales differ. Convert the area instead. For a crop saved at 0°, 90°, 180° or 270°, take the percentages from croppedArea: center.x is (x + width / 2) / 100, center.y is (y + height / 2) / 100, size.width is width / 100, size.height is height / 100, quarterTurns is the angle divided by 90, and rotation is 0.
For another angle, use croppedAreaPixels and the photo's natural size. Take the bounding box of the photo rotated by the saved angle. Measure the offset of the crop's centre from the centre of that box, in pixels. Set quarterTurns to the angle rounded to the nearest quarter turn, wrapped to a count from 0 to 3, and rotation to what is left. Divide the offset and the crop's width and height by the sides of the photo after its quarter turns, and add 0.5 to each offset to get center. Set mirrored to false, both perspective fields to 0 and aspect to your ratio. Value lists every field.
We ran the pixel conversion against @zriz/core 0.2.1 on a 2400 by 1600 px photo with one aspect and no flip. The percentage form for quarter turns follows from it, and we did not run it on its own. For seven crops that lie inside the photo, at 0°, 90°, 180°, 270°, 30°, -12° and 100°, zriz picked a source pixel within 0.5 px of react-easy-crop's for every crop corner, and the output size matched within 1 px. A crop that reaches outside the photo does not survive. A 10° crop of 2400 by 1800 px came back as 1753 by 1314 px. A flip made with transform is not in the react-easy-crop data, so carry it over by hand.
Install @zriz/react and @zriz/core. Replace <Cropper /> with the parts below. Drop the crop, zoom and rotation state unless the app reads it elsewhere, because zriz works uncontrolled. Move the save code from onCropComplete and getCroppedImg to one exportCrop call. Convert the stored crops. Then remove react-easy-crop and the copied canvas function.
"use client";
import { exportCrop } from "@zriz/core/dom";
import type { Cropper as CropperApi } from "@zriz/react";
import * as Cropper from "@zriz/react/cropper";
import "@zriz/react/styles.css";
import { useRef, useState } from "react";
type Props = {
src: string | File;
onSave: (jpeg: Blob) => void;
};
export const PhotoCropper = ({ src, onSave }: Props) => {
const cropper = useRef<CropperApi>(null);
const [ready, setReady] = useState(false);
const [failed, setFailed] = useState(false);
const save = async () => {
if (!cropper.current) return;
try {
const { blob } = await exportCrop(cropper.current, {
type: "image/jpeg",
maxSize: { width: 1600, height: 1200 },
});
setFailed(false);
onSave(blob);
} catch {
setFailed(true);
}
};
return (
<>
<Cropper.Root
actionsRef={cropper}
src={src}
aspect={4 / 3}
maxZoom={3}
onLoad={() => setReady(true)}
onError={() => setReady(false)}
>
<Cropper.Viewport
wheel="zoom"
rotateGesture={false}
style={{ maxWidth: 480, height: 360 }}
>
<Cropper.Image />
<Cropper.Mask />
<Cropper.Area aria-label="Crop area" />
</Cropper.Viewport>
<Cropper.Zoom
aria-label="Zoom"
style={{ width: 240, height: 24 }}
>
<Cropper.ZoomThumb />
</Cropper.Zoom>
</Cropper.Root>
{failed && <p role="alert">This crop did not save.</p>}
<button type="button" disabled={!ready} onClick={save}>
Save
</button>
</>
);
};This is the basic react-easy-crop screen in zriz: a 4:3 frame with no handles, a photo that pans and zooms under it, a zoom slider and a save button. wheel="zoom" keeps the plain wheel on zoom. maxZoom={3} sets the same number as the react-easy-crop default, on the zriz zoom scale. rotateGesture={false} turns off the two-finger twist, which the old screen had only with onRotationChange. exportCrop returns a JPEG no larger than 1600 by 1200 px.
The app still writes the paint for the Zoom track, the thumb and the button, the layout around the cropper, and every label. Add Dial, Rotate or Flip if the old screen had a rotation control, and remove rotateGesture={false} if it passed onRotationChange. Blocks for shadcn/ui are on the shadcn page.
How we checked
We installed the published npm package at the version above and read its source and docs. Then we ran both libraries in headless Chromium with the same photo and the same script: tab order, keys, ARIA attributes, mouse, wheel, touch and a 15° turn. The export step ran for zriz alone, because react-easy-crop has no export call.
react-easy-crop lives in the GitHub repo, its docs and its npm page.
More comparisons
- zriz vs react-image-cropThe other model: a box with handles that the user draws over a still image
- zriz vs react-avatar-editorAnother fixed frame with the photo moving under it, drawn on one canvas
- zriz vs antd-img-cropreact-easy-crop inside an Ant Design modal, wired to the antd
Upload
Every comparison is on the comparison page.