Skip to navigation

zriz vs antd-img-crop

antd-img-crop wraps an Ant Design Upload and opens a modal with a cropper before the file uploads. zriz is a set of unstyled cropper parts with no upload step and no modal.

We make zriz, so read this page with that in mind. It compares antd-img-crop 4.30.0 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 antd-img-crop to zriz. Read https://zriz.dev/docs/zriz-vs-antd-img-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

antd-img-crop calls itself "An image cropper for Ant Design Upload" in its README. You wrap an antd Upload in ImgCrop. When a person picks a file, an antd modal opens with a react-easy-crop cropper inside, and OK sends the cropped file on to the upload. The Ant Design Upload docs use it in their crop demo.

Pick antd-img-crop when the app is on Ant Design and the crop is a step before an upload. One wrapper gives the modal, the sliders, OK and Cancel as antd components, and the app writes no crop code. Its optional sliders turn the photo through 180° each way and change the aspect, from 0.5 to 2 by default, and zriz has neither as one control. It adds about 10 kB to an app that already uses antd's Modal, Upload, Button and Slider. Its peer range starts at React 16.8, and we ran it on React 19 alone. It has been on npm since 2018 and has over 100k downloads a week.

Pick zriz when the app has no antd, or the crop is not part of an upload. zriz opens a URL, a File or a Blob, has handles on the crop, quarter turns and flip, and gives the crop back as a value you can save and restore. A round crop can export as a round PNG. You write the dialog, the styles and every label, and zriz adds 22 to 30 kB. zriz had its first release in November 2026 and has under a thousand downloads a week. Each package has one maintainer on npm.

Numbers

Factantd-img-cropzriz
First release2018November 2026
npm downloads a week100k+Under 1,000
GitHub stars540+Under 100
LicenceMITMIT
Dependenciesreact-easy-crop and tslib, with antd as a peerNone, with @zriz/core as a peer
React16.8 and newer by its peer range, we ran it on 1918.2 and 19
SizeAbout 10 kB, on top of antd22 to 30 kB

Size is minified JavaScript and CSS, compressed with Brotli, without React. For antd-img-crop it is the basic example from its docs, built with esbuild. That number leaves out antd's modal, slider and buttons. With antd's Modal, Upload, Button and Slider counted the same example is about 111 kB, and an app pays for whichever of those four it does not use yet. 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

Featureantd-img-cropzriz
Crop box with handlesNoA fixed frame in the centreYesFour corners and four edges
Photo moves under the frameYesYes
Aspect ratioYesOne ratio or a slider, 0.5 to 2 by defaultYesFree, original or any ratio
Round cropPartlyRound mask, the file is a rectangleYesMask and export
ZoomYesWheel, pinch, slider and step buttonsYesPinch, Ctrl + wheel, slider part, keys
Straighten (free rotation)YesOptional slider, 180° each wayPartly45° each way plus quarter turns
Crop stays inside the photo at an angleNoThe frame can take in empty corners, which export in fillColorYesThe crop shrinks to fit
Quarter turns and flipPartly90° on the optional rotation slider, no flipYesAnimated
Touch gesturesYesPan, pinch, twist. The twist reaches the file only with rotationSliderYesPan, pinch, twist, two handles at once
KeyboardYesArrows pan the photo, sliders, buttonsYesCrop area, dial, zoom and ratio group; handles no
Screen readerPartlyDialog and slider roles, sliders have no namesPartlyRoles and live region; you write the labels
Image exportYesA File labelled with the picked file's typeYesJPEG, PNG or WebP blob
Saved cropPartlyReads out a pixel rectangle through cropperPropsYesFractions of the photo, restores exactly
Ready-made UIYesAntd modal, sliders, OK and CancelNoUnstyled parts and an optional stylesheet

The rotation slider, the aspect slider and the Reset button in antd-img-crop are off by default. rotationSlider, aspectSlider and showReset turn them on.

antd-img-crop 4.30.0 installs react-easy-crop 5, which is the cropper inside the modal. What the react-easy-crop page says about version 6 does not carry over.

What you will notice

antd-img-crop takes less code. The whole crop step is ImgCrop around an Upload. The modal, the sliders and the OK, Cancel and Reset buttons are antd's own components, OK and Cancel come from antd's locale, and the cropped file lands in the upload request. With zriz you build the dialog, the zoom slider, the buttons and the hand-off to the upload.

antd-img-crop has one way in and one way out. The photo is a file that passes through beforeUpload of the wrapped Upload, and the result is a new File. It has no src prop for a photo the app already has. zriz takes a URL, a File or a Blob, and returns a blob from exportCrop plus a value that reopens the same crop later.

A plain mouse wheel zooms in antd-img-crop, and in Chromium the page did not scroll while it did. In zriz a plain wheel pans the photo and Ctrl + wheel zooms. Set wheel="zoom" on Viewport to get the antd-img-crop behaviour.

The antd-img-crop rotation slider runs through 180° each way. At an angle the frame can take in area outside the photo, and the export code paints that area with fillColor, white by default. zriz stops the straighten at 45° each way and shrinks the crop so the saved value and the exported file hold no empty corner. Larger angles in zriz take a quarter turn plus a straighten.

antd-img-crop asks the browser for the picked file's type and labels the file with it, at the crop's full pixel size, with quality 0.4 unless you set it. A round crop there is a round mask, and the file is the rectangle around it. zriz defaults to JPEG at quality 0.9, or at the photo's own quality when a JPEG file's is lower. It has type and maxSize options, and exports a circle as a PNG with transparent corners when type is left out. zriz scales an export down above about 16.8 megapixels on a device with a touch screen and about 268 megapixels elsewhere.

In antd-img-crop the three sliders have role="slider" and no accessible name, the step buttons are named by their glyphs, and no prop sets either. The dialog role, the link from the dialog to its "Edit image" title and the focus trap come from antd. zriz ships no words, so every label is yours to write, and a development build warns when the crop area, dial or zoom has no name.

Migrate from antd-img-crop

zriz has no equivalent for the wrapper itself. It has no modal, no title, no OK and Cancel, and no hook into antd Upload, so you write those. It has no single slider through 180° each way. It has no crop that reaches outside the photo and no fillColor for empty corners. It has no minZoom, because zoom starts at 1, the largest crop that fits. It needs React 18.2 or newer and has no CommonJS build.

  • <ImgCrop> around UploadYour own dialog with Cropper.Root inside, opened from beforeUpload
  • aspect, default 1aspect on Root, default "free"
  • cropShape="round"shape="circle" on Root. The file is a round PNG when exportCrop gets no type, so drop type from the snippet
  • showGridCropper.Grid inside Area. It shows while the crop moves and hides at rest
  • zoomSliderZoom with ZoomThumb, two ZoomStep buttons, and wheel="zoom" on Viewport
  • maxZoom, default 3maxZoom on Root, default 15
  • minZoomNo equivalent
  • rotationSliderDial for 45° each way, Rotate for quarter turns
  • aspectSlider, minAspect, maxAspectaspect on Root from a range input of your own, or AspectGroup presets
  • showReset, resetTextCropper.Reset with your text. It also puts the position back
  • quality, default 0.4quality in exportCrop, default 0.9, or the photo's own for a JPEG file when that is lower
  • fillColorNo equivalent. A JPEG export paints clear pixels white
  • cropperPropsProps on the zriz parts
  • modalTitle, modalWidth, modalOk, modalCancel, modalClassName, modalPropsProps of your own dialog
  • onModalOkYour OK handler, after exportCrop resolves
  • onModalCancelYour cancel handler
  • beforeCropA check of your own before you open the dialog
  • The forwarded refactionsRef on Root

antd-img-crop saves no crop. Its output is a re-encoded File, so most apps have nothing to convert. An app that kept croppedAreaPixels from cropperProps.onCropAreaChange has { x, y, width, height } in source pixels from the top left of the photo. For a crop with no rotation, the zriz value is center: { x: (x + width / 2) / W, y: (y + height / 2) / H } and size: { width: width / W, height: height / H }, where W and H are the photo's pixel size. We checked the zriz side of that formula by running code. This page gives no conversion for a rotated crop. antd-img-crop has no prop or callback that reports the angle, and we did not check the formula above with rotation. zriz has no importer.

Install @zriz/react and @zriz/core. Take ImgCrop away from around Upload. Give Upload a beforeUpload that returns a promise, and keep the picked file and the promise's resolve in state. Render the dialog below while that state is set. Resolve with the cropped file from onCrop, and with Upload.LIST_IGNORE from onCancel. This is the same hand-off antd-img-crop does inside. Then add the sliders and buttons your ImgCrop props turned on, from the table above.

"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 { useEffect, useRef, useState } from "react";

type Props = {
  file: File;
  onCancel: () => void;
  onCrop: (cropped: File) => void;
};

const TYPES = [
  "image/jpeg",
  "image/png",
  "image/webp",
] as const;
const unreadable = "This photo does not open. Pick another.";
const unsaved = "This crop did not save. Try again.";

export const CropDialog = ({
  file,
  onCancel,
  onCrop,
}: Props) => {
  const dialog = useRef<HTMLDialogElement>(null);
  const actions = useRef<CropperApi>(null);
  const [ready, setReady] = useState(false);
  const [saving, setSaving] = useState(false);
  const [problem, setProblem] = useState<string | null>(null);

  useEffect(() => {
    const node = dialog.current;

    node?.showModal();

    return () => node?.close();
  }, []);

  const save = async () => {
    if (!actions.current || saving) return;
    const type =
      TYPES.find((item) => item === file.type) ?? "image/jpeg";

    setSaving(true);

    try {
      const result = await exportCrop(actions.current, {
        type,
      });

      onCrop(
        new File([result.blob], file.name, {
          type: result.type,
        })
      );
    } catch {
      setProblem(unsaved);
      setSaving(false);
    }
  };

  return (
    <dialog
      ref={dialog}
      aria-label="Edit image"
      onCancel={onCancel}
    >
      <Cropper.Root
        actionsRef={actions}
        src={file}
        aspect={1}
        maxZoom={3}
        onLoad={() => setReady(true)}
        onError={() => setProblem(unreadable)}
      >
        <Cropper.Viewport
          wheel="zoom"
          rotateGesture={false}
          style={{ width: 480, height: 320 }}
        >
          <Cropper.Image />
          <Cropper.Mask />
          <Cropper.Area aria-label="Crop area" />
        </Cropper.Viewport>
        <Cropper.ZoomStep factor={0.8} aria-label="Zoom out">
          −
        </Cropper.ZoomStep>
        <Cropper.Zoom
          aria-label="Zoom"
          style={{ width: 240, height: 24 }}
        >
          <Cropper.ZoomThumb />
        </Cropper.Zoom>
        <Cropper.ZoomStep factor={1.25} aria-label="Zoom in">
          +
        </Cropper.ZoomStep>
      </Cropper.Root>
      {problem && <p role="alert">{problem}</p>}
      <button type="button" onClick={onCancel}>
        Cancel
      </button>
      <button
        type="button"
        disabled={!ready}
        aria-disabled={saving}
        onClick={save}
      >
        OK
      </button>
    </dialog>
  );
};

CropDialog follows the antd-img-crop defaults: a square frame with no handles, a zoom slider with two step buttons, zoom up to 3×, a wheel that zooms, Cancel and OK. rotateGesture={false} on Viewport stops a two-finger twist from turning the photo, because antd-img-crop leaves rotation out of the file while rotationSlider is off. Remove it when you add a Dial. Some behaviour differs. The step buttons zoom by a factor, where antd-img-crop steps by 0.1. On the crop area, arrows move the crop by 1% of its size where antd-img-crop moves the photo 1 px, Alt + arrows resize the frame, and +, - and 0 zoom and reset. The dialog has no visible title and no close button. OK exports the crop in the picked file's type when that is JPEG, PNG or WebP, and as JPEG otherwise, under the picked file's name. Read type on the result before you trust the file extension.

The app still writes the styles for the dialog, the zoom track and thumb and the buttons. An antd Modal, Button and theme tokens can do that work. The labels in the snippet are English, so translate them where the app has other languages. The default quality goes from 0.4 to 0.9, or to the photo's own quality for a JPEG file when that is lower. Pass quality: 0.4 to exportCrop to keep the old setting.

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 and touch. The package has no call that sets an angle or exports a file on demand, and the script did not press OK, so the statements about its rotation and its export come from its source. The run and the sizes used antd 6.7.0 and React 19, and we checked nothing on antd 4 or 5.

antd-img-crop has a repo whose README is its documentation, and an npm page. The Ant Design Upload docs show it in use.

More comparisons

Every comparison is on the comparison page.