# zriz vs react-easy-crop

> [llms.txt](https://zriz.dev/llms.txt)

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](https://github.com/pqoqubbw/zriz/issues) and we will fix it.

To move a project over, paste this into Claude Code, Cursor or another coding agent:

```text
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](https://github.com/ValentinH/react-easy-crop) 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](https://zriz.dev/docs/bundle-size.md) has each set. `@zriz/react` needs `@zriz/core` and React as peers.

## Features

| Feature | react-easy-crop | zriz |
| - | - | - |
| Crop box with handles | No. The frame is fixed | Yes. Four corners and four edges |
| Photo moves under the frame | Yes | Yes |
| Aspect ratio | Partly. Any number, no free ratio | Yes. Free, original or any ratio |
| Round crop | Partly. Round mask, the output stays a rectangle | Yes. Mask and export |
| Zoom | Yes. Wheel, pinch and the `zoom` prop | Yes. Pinch, Ctrl + wheel, slider part, keys |
| Straighten (free rotation) | Yes. Any angle, the app builds the control | Partly. 45° each way plus quarter turns |
| Crop stays inside the photo at an angle | No. The frame can show empty area | Yes. The crop shrinks to fit |
| Quarter turns and flip | Partly. Turns through `rotation`, no flip in output | Yes. Animated |
| Touch gestures | Yes. Pan, pinch, twist with `onRotationChange` | Yes. Pan, pinch, twist, two handles at once |
| Keyboard | Partly. Arrow keys move the photo | Yes. Crop area, dial, zoom and ratio group; handles no |
| Screen reader | No. It sets no role or ARIA attribute | Partly. Roles and live region; you write the labels |
| Image export | No. Crop numbers and a docs function | Yes. JPEG, PNG or WebP blob |
| Saved crop | Yes. Percentages, the app stores the angle | Yes. Fractions of the photo, restores exactly |
| Ready-made UI | Partly. Styled frame, mask and grid, no controls | No. Unstyled 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](https://github.com/ValentinH/react-easy-crop/blob/v6.2.5/src/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](https://github.com/ValentinH/react-easy-crop/issues/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](https://github.com/ValentinH/react-easy-crop/blob/v6.2.5/src/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
- `image`. `src` on `Root`, a URL, `File` or `Blob`
- `video`. No equivalent
- `crop`, `onCropChange`. No prop. The position is `value.center`, and `panBy()` moves it
- `zoom`, `onZoomChange`. `zoom` on `Root` with `onValueChange`, or the `Zoom` part
- `minZoom`. No equivalent. Zoom starts at 1, the largest crop that fits
- `maxZoom`, default 3. `maxZoom` on `Root`, default 15
- `rotation`, `onRotationChange`. `value.rotation` up to 45° each way plus `value.quarterTurns`, set by `Dial` and `Rotate`
- `aspect`. `aspect` on `Root`, a number, `"free"` or `"original"`
- `cropShape="round"`. `shape="circle"`, which also rounds the export
- `cropSize`, `objectFit`. No equivalent
- `zoomSpeed`, `onCropSizeChange`, `roundCropAreaPixels`. No equivalent
- `restrictPosition={false}`. No equivalent
- `showGrid`. The `Grid` part, shown during gestures
- `zoomWithScroll`. `wheel` on `Viewport`, one of `"pan"`, `"zoom"` or `"none"`
- `onWheelRequest`. `wheel` 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
- `onTouchRequest`. `details.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`, `onInteractionEnd`. `gesturestart` and `gestureend` events on the cropper, or `data-interacting` on `Root`
- `onCropComplete`. `onValueCommit`, with the pixel plan in `details.pixelCrop`. It does not fire for the first photo until the user changes the crop
- `onCropAreaChange`. `onValueChange`
- `onMediaLoaded`. `onLoad`
- `initialCroppedAreaPercentages`, `initialCroppedAreaPixels`. `defaultValue` or `value`, after the conversion below
- `getInitialCropFromCroppedAreaPercentages`, `getInitialCropFromCroppedAreaPixels`. Not needed. A zriz value restores itself
- `setImageRef`, `setVideoRef`, `setCropperRef`. A `ref` on the part
- `transform` used for a flip. The `Flip` part and `value.mirrored`
- `classes`, `style`. `className` and `style` on each part
- `mediaProps`. Props on `Image`
- `cropperProps`. Props on `Area`, such as `aria-label`
- `disableAutomaticStylesInjection`, `nonce`. Not needed. zriz injects no `style` tag, and its stylesheet is an optional import
- `keyboardStep`. No prop. Arrows move the crop by 1% of its size, 10% with Shift
- `getCroppedImg` from the docs. `exportCrop(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](https://zriz.dev/docs/value.md) 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.

```tsx
"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](https://zriz.dev/docs/shadcn.md) 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](https://github.com/ValentinH/react-easy-crop), its [docs](https://valentinh.github.io/react-easy-crop/) and its [npm page](https://www.npmjs.com/package/react-easy-crop).

## More comparisons

- [zriz vs react-image-crop](https://zriz.dev/docs/zriz-vs-react-image-crop.md). The other model: a box with handles that the user draws over a still image
- [zriz vs react-avatar-editor](https://zriz.dev/docs/zriz-vs-react-avatar-editor.md). Another fixed frame with the photo moving under it, drawn on one canvas
- [zriz vs antd-img-crop](https://zriz.dev/docs/zriz-vs-antd-img-crop.md). react-easy-crop inside an Ant Design modal, wired to the antd `Upload`

Every comparison is on the [comparison](https://zriz.dev/docs/comparison.md) page.
