Skip to navigation

zriz vs react-advanced-cropper

react-advanced-cropper is a styled React cropper with replaceable parts and algorithms, where a selection box moves over the photo. zriz is a set of unstyled parts where the photo moves under the crop and the saved crop stays inside the photo at any straighten angle.

We make zriz, so read this page with that in mind. It compares react-advanced-cropper 0.20.1 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-advanced-cropper to zriz. Read https://zriz.dev/docs/zriz-vs-react-advanced-cropper.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-advanced-cropper is a styled React cropper whose parts and algorithms you can replace. Its README calls it "the advanced library that gives you opportunity to create your own croppers suited for any website design". The state lives in a core without a framework, advanced-cropper. The docs are for the React package, and their tutorials use the core's extensions.

Pick react-advanced-cropper when the visitor should drag a selection box over a still photo, when the crop needs a range of aspect ratios, a largest size in pixels or room past the edge of the photo with a fill colour, or when you want your own stencil shape and your own resize rules. It runs on React 16.8 and newer, ships five themes, a preview part and a CommonJS build, and returns a canvas you can draw on. It is about 24 kB against 22 to 30 kB for zriz. It has been on npm since 2021 and has over 250k downloads a week.

Pick zriz when the cropper has to work from the keyboard and with a screen reader, when visitors straighten photos and no corner of the saved crop may be empty, or when you want parts for the dial, zoom slider, aspect picker, rotate and flip buttons instead of building them. zriz takes a File as src and returns a JPEG, PNG or WebP blob. It is new. Its first release was in November 2026, it has one maintainer and under a thousand downloads a week.

Numbers

Factreact-advanced-cropperzriz
First release2021November 2026
npm downloads a week250k+Under 1,000
GitHub stars880+Under 100
LicenceMITMIT
Dependencies3: advanced-cropper, classnames, tslibNone
React16.8 and newer18.2 and 19
SizeAbout 24 kB22 to 30 kB

Size is minified JavaScript and CSS, compressed with Brotli, without React. For react-advanced-cropper it is the basic example from its docs, built with esbuild. That example reads a canvas on every change, and its stylesheet is about 1 kB of the total. 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-advanced-cropperzriz
Crop box with handlesYesFour corners and four edgesYesFour corners and four edges
Photo moves under the frameYesDrag outside the box, or FixedCropperYes
Aspect ratioYesFree, one ratio or a rangeYesFree, original or any ratio
Round cropPartlyRound mask, the canvas stays squareYesMask and export
ZoomYesWheel, pinch and zoomImageYesPinch, Ctrl + wheel, slider part, keys
Straighten (free rotation)YesAny angle, no dial partPartly45° each way plus quarter turns
Crop stays inside the photo at an anglePartlyNot by default. The core's fitStencilToImage, passed as postProcess, does itYesThe crop shrinks to fit
Quarter turns and flipYesAnimated, through ref methodsYesAnimated
Touch gesturesYesPan, pinch, handles, twist when enabledYesPan, pinch, twist, two handles at once
KeyboardNoYesCrop area, dial, zoom and ratio group; handles no
Screen readerNoPartlyRoles and live region; you write the labels
Image exportPartlyA canvas, you call toBlobYesJPEG, PNG or WebP blob
Saved cropYesPixel coordinates and transforms through default propsYesFractions of the photo, restores exactly
Ready-made UIPartlyStyled crop surface and five themesNoUnstyled parts and an optional stylesheet

The No cells for keyboard and screen reader come from a search of the published 0.20.1 bundle and both source trees for tabIndex, key handlers, focus, role, aria- and alt. None of them matched. In Chromium, Tab reached no element inside the cropper.

For zriz, the crop stays inside the photo in the saved value and the exported file. While a finger pulls the photo past an edge, the background shows until release.

What you will notice

A drag does different things. By its source, in react-advanced-cropper a drag that starts inside the box moves the box, and a drag that starts outside it pans the photo, because the stencil stops the event before the background sees it. In zriz every drag on the photo pans it under the crop. After a handle drag the crop moves back to the centre 300 ms after release. If your design is a selection box on a still photo, react-advanced-cropper does that by default and zriz has no mode for it.

A free angle can leave empty corners in react-advanced-cropper. Its limits measure against the bounding box of the rotated photo, so the box can cover the triangles between that box and the photo. In Chromium, at 15° with the default box, two corners of its exported PNG were transparent. At the same angle zriz shrank the crop to 74% of the photo's width and height, about 55% of its area, and all four corners of its exported JPEG held photo pixels. Its docs recommend multiples of 90. The core does ship fitStencilToImage in advanced-cropper/showcase/mobile, built on its fit-to-image helpers. Passed as postProcess it keeps the crop inside, and the same author's react-mobile-cropper does that. zriz shrinks the crop to fit at every angle, and its straighten angle stops at 45° each way.

The wheel zooms in one and pans in the other. In react-advanced-cropper each wheel event zooms by a fixed 10%, with or without Ctrl. Its handler calls preventDefault, so the page does not scroll while the pointer is over the cropper, which its docs describe. In zriz a plain wheel pans the photo and Ctrl + wheel zooms. wheel="zoom" on Viewport makes every wheel event zoom.

The two libraries ship different halves of an editor. react-advanced-cropper ships a painted crop surface that works after one CSS import, with five themes and a CropperPreview part. Your app writes the zoom slider, rotate and flip buttons, any angle dial and the aspect picker. zriz ships those controls as unstyled parts with roles and keys, plus an optional stylesheet of about 1 kB for the frame, handles, grid and dial. Your app writes the layout, the slider and button styles and every label.

Export hands you different things. getCanvas() runs on the main thread and returns an HTMLCanvasElement at once, or null before the image loads. You call toBlob on it or draw on it yourself. exportCrop is async, takes a type, a quality and a maxSize, and resolves an object with the blob. It runs in a worker by default and falls back to the main thread where workers or OffscreenCanvas are missing or blocked. zriz gives you no canvas.

Migrate from react-advanced-cropper

zriz has no equivalent for a selection box that moves over a still photo, aspect ratio ranges, a largest crop size or size limits as a function, crops that extend past the photo with a fill colour, custom stencil components, replaceable move, resize and transform algorithms, a frame size in screen pixels as in FixedCropper, a view that zooms apart from the crop, the coordinates-only mode its docs describe for images from a server without CORS headers, a preview part, a CommonJS or script tag build, browsers older than the zriz FAQ lists, or React 16.8 and 17. If the project depends on one of these, stay.

  • <Cropper src>Root src with Viewport, Image, Mask and Area inside
  • react-advanced-cropper/dist/style.css@zriz/react/styles.css, optional
  • stencilComponent={CircleStencil}shape="circle" on Root
  • stencilProps.aspectRatio as a numberaspect on Root
  • stencilProps.handlersRender the Handle parts you want
  • stencilProps.gridGrid inside Area
  • stencilProps.movableNo equivalent, a drag pans the photo
  • minWidth, minHeightminSize on Root
  • defaultCoordinates, defaultTransformsdefaultValue on Root
  • backgroundWrapperProps.scaleImage.wheelwheel on Viewport: "pan", "zoom" or "none"
  • backgroundWrapperProps.rotateImagerotateGesture on Viewport, on by default
  • ref with CropperRefactionsRef on Root
  • getCoordinates()getPixelCrop()?.pixelRect, in the upright original, null at a straighten angle
  • getTransforms()rotation, quarterTurns and mirrored on the value
  • getState(), setState()getValue(), setValue() or the value prop
  • getCanvas() then toBlobexportCrop(cropper, { type, quality, maxSize })
  • zoomImage(factor)setZoom(getZoom() * factor) or Zoom
  • rotateImage(90), rotateImage(-90)rotate90("cw"), rotate90()
  • rotateImage(angle) at a free anglesetRotation(getValue().rotation + angle) within 45° each way, or Dial
  • flipImage(true, false), flipImage(false, true)flip(), flip("y")
  • reset()reset() or Reset
  • refresh()Nothing to call
  • onChangeonValueChange
  • onInteractionEnd, onMoveEnd, onResizeEndonValueCommit
  • onReady, onErroronLoad, onError
  • disableddisabled

Apps usually store getCoordinates() and getTransforms(). That is a rectangle in pixels of the natural image, measured from the top left of the bounding box of the rotated image, plus rotate in degrees clockwise and two flip booleans. zriz ships no importer, so the app converts once. Add 180 to rotate when the vertical flip is on. The nearest multiple of 90 becomes the quarter turns and the remainder becomes the straighten angle. The mirror is on when exactly one of the two flips is on. The crop centre is left + width / 2 and top + height / 2, minus half the bounding box. Build a state of { image, turns, angle, flip, crop: { x, y, width, height }, aspect, perspective }, where image is the natural size and crop holds that centre. Our script set aspect to null and perspective to { horizontal: 0, vertical: 0, focal: 1733 } for its 2400 by 1600 test photo. stateToValue does not copy focal into the value. stateToValue(state, "free") from @zriz/core turns it into a CropValue.

We ran this conversion in a Node script against both published cores for 10 angles and the 4 flip combinations. In all 40 cases the four crop corners landed on the same source pixels. We compared no exported files in a browser. It holds for crops that sit inside the photo and for photos without an EXIF turn. react-advanced-cropper puts an EXIF orientation into its starting rotate and flip. We did not run this conversion on a photo with an EXIF turn, so check those crops by eye. A crop saved at a free angle or with imageRestriction="none" can include area outside the photo, and zriz shrinks or moves it. See value for the format.

Install @zriz/react and @zriz/core. Replace <Cropper> with the parts below and swap the stylesheet import. Move the ref to actionsRef, replace the getCanvas() and toBlob pair with exportCrop, and rename the callbacks from the table. Callbacks in zriz receive the value and a details object, not the cropper. Convert stored crops, then remove react-advanced-cropper and its theme imports.

"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; onSave: (photo: Blob) => void };

export const PhotoCropper = ({ src, onSave }: Props) => {
  const [ready, setReady] = useState(false);
  const [problem, setProblem] = useState("");
  const actions = useRef<CropperApi>(null);

  const save = async () => {
    if (!actions.current) return;

    try {
      const { blob } = await exportCrop(actions.current, {
        type: "image/jpeg",
        quality: 0.9,
      });

      setProblem("");
      onSave(blob);
    } catch {
      setProblem("This crop did not save. Try again.");
    }
  };

  return (
    <>
      <Cropper.Root
        actionsRef={actions}
        src={src}
        onLoad={() => setReady(true)}
        onError={() => setProblem("This photo does not open.")}
      >
        <Cropper.Viewport
          wheel="zoom"
          rotateGesture={false}
          style={{ maxWidth: 480, height: 360, padding: 24 }}
        >
          <Cropper.Image />
          <Cropper.Mask />
          <Cropper.Area aria-label="Crop area">
            <Cropper.Handle position="top-left" />
            <Cropper.Handle position="top" />
            <Cropper.Handle position="top-right" />
            <Cropper.Handle position="right" />
            <Cropper.Handle position="bottom-right" />
            <Cropper.Handle position="bottom" />
            <Cropper.Handle position="bottom-left" />
            <Cropper.Handle position="left" />
          </Cropper.Area>
        </Cropper.Viewport>
      </Cropper.Root>
      {problem && <p role="alert">{problem}</p>}
      <button type="button" disabled={!ready} onClick={save}>
        Save
      </button>
    </>
  );
};

This is the default Cropper rebuilt: a free crop box with eight handles over a photo from a URL, and a save button that hands onSave a JPEG. wheel="zoom" keeps the wheel zooming and rotateGesture={false} keeps the two-finger twist off, as in react-advanced-cropper's defaults. A drag inside the box still pans the photo. The app writes the size of the viewport, the button styles, the two messages and the label on Area. To add a zoom slider, a dial or rotate buttons, see full editor.

How we checked

We installed the published npm package at the version above and read its source and docs, plus the source of react-mobile-cropper for the one sentence about it. 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. We turned on backgroundWrapperProps.rotateImage, which is off by default, to test the twist.

Sources: the repository, the core repository, the docs and the npm page.

More comparisons

Every comparison is on the comparison page.