Skip to navigation

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

Factreact-easy-cropzriz
First release2018November 2026
npm downloads a week5.5M+Under 1,000
GitHub stars2.7k+Under 100
LicenceMITMIT
DependenciesOne, normalize-wheelNone
React16.4 and newer18.2 and 19
SizeAbout 7 kB22 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

Featurereact-easy-cropzriz
Crop box with handlesNoThe frame is fixedYesFour corners and four edges
Photo moves under the frameYesYes
Aspect ratioPartlyAny number, no free ratioYesFree, original or any ratio
Round cropPartlyRound mask, the output stays a rectangleYesMask and export
ZoomYesWheel, pinch and the zoom propYesPinch, Ctrl + wheel, slider part, keys
Straighten (free rotation)YesAny angle, the app builds the controlPartly45° each way plus quarter turns
Crop stays inside the photo at an angleNoThe frame can show empty areaYesThe crop shrinks to fit
Quarter turns and flipPartlyTurns through rotation, no flip in outputYesAnimated
Touch gesturesYesPan, pinch, twist with onRotationChangeYesPan, pinch, twist, two handles at once
KeyboardPartlyArrow keys move the photoYesCrop area, dial, zoom and ratio group; handles no
Screen readerNoIt sets no role or ARIA attributePartlyRoles and live region; you write the labels
Image exportNoCrop numbers and a docs functionYesJPEG, PNG or WebP blob
Saved cropYesPercentages, the app stores the angleYesFractions of the photo, restores exactly
Ready-made UIPartlyStyled frame, mask and grid, no controlsNoUnstyled 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 />Root with Viewport, Image, Mask and Area inside
  • imagesrc on Root, a URL, File or Blob
  • videoNo equivalent
  • crop, onCropChangeNo prop. The position is value.center, and panBy() moves it
  • zoom, onZoomChangezoom on Root with onValueChange, or the Zoom part
  • minZoomNo equivalent. Zoom starts at 1, the largest crop that fits
  • maxZoom, default 3maxZoom on Root, default 15
  • rotation, onRotationChangevalue.rotation up to 45° each way plus value.quarterTurns, set by Dial and Rotate
  • aspectaspect on Root, a number, "free" or "original"
  • cropShape="round"shape="circle", which also rounds the export
  • cropSize, objectFitNo equivalent
  • zoomSpeed, onCropSizeChange, roundCropAreaPixelsNo equivalent
  • restrictPosition={false}No equivalent
  • showGridThe Grid part, shown during gestures
  • zoomWithScrollwheel on Viewport, one of "pan", "zoom" or "none"
  • onWheelRequestwheel on Viewport. wheel="none" leaves a plain wheel to the page and keeps Ctrl + wheel for zoom. details.cancel() in onValueChange rejects a change and does not scroll the page
  • onTouchRequestdetails.cancel() in onValueChange rejects the change. It does not hand the touch back to the page, so a finger on the cropper never scrolls it
  • onInteractionStart, onInteractionEndgesturestart and gestureend events on the cropper, or data-interacting on Root
  • onCropCompleteonValueCommit, with the pixel plan in details.pixelCrop. It does not fire for the first photo until the user changes the crop
  • onCropAreaChangeonValueChange
  • onMediaLoadedonLoad
  • initialCroppedAreaPercentages, initialCroppedAreaPixelsdefaultValue or value, after the conversion below
  • getInitialCropFromCroppedAreaPercentages, getInitialCropFromCroppedAreaPixelsNot needed. A zriz value restores itself
  • setImageRef, setVideoRef, setCropperRefA ref on the part
  • transform used for a flipThe Flip part and value.mirrored
  • classes, styleclassName and style on each part
  • mediaPropsProps on Image
  • cropperPropsProps on Area, such as aria-label
  • disableAutomaticStylesInjection, nonceNot needed. zriz injects no style tag, and its stylesheet is an optional import
  • keyboardStepNo prop. Arrows move the crop by 1% of its size, 10% with Shift
  • getCroppedImg from 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

Every comparison is on the comparison page.