# zriz vs antd-img-crop

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

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](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 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](https://github.com/nanxiaobei/antd-img-crop#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](https://ant.design/components/upload) 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

| Fact | antd-img-crop | zriz |
| - | - | - |
| First release | 2018 | November 2026 |
| npm downloads a week | 100k+ | Under 1,000 |
| GitHub stars | 540+ | Under 100 |
| Licence | MIT | MIT |
| Dependencies | `react-easy-crop` and `tslib`, with `antd` as a peer | None, with `@zriz/core` as a peer |
| React | 16.8 and newer by its peer range, we ran it on 19 | 18.2 and 19 |
| Size | About 10 kB, on top of antd | 22 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](https://zriz.dev/docs/bundle-size.md) has each set. `@zriz/react` needs `@zriz/core` and React as peers.

## Features

| Feature | antd-img-crop | zriz |
| - | - | - |
| Crop box with handles | No. A fixed frame in the centre | Yes. Four corners and four edges |
| Photo moves under the frame | Yes | Yes |
| Aspect ratio | Yes. One ratio or a slider, 0.5 to 2 by default | Yes. Free, original or any ratio |
| Round crop | Partly. Round mask, the file is a rectangle | Yes. Mask and export |
| Zoom | Yes. Wheel, pinch, slider and step buttons | Yes. Pinch, Ctrl + wheel, slider part, keys |
| Straighten (free rotation) | Yes. Optional slider, 180° each way | Partly. 45° each way plus quarter turns |
| Crop stays inside the photo at an angle | No. The frame can take in empty corners, which export in `fillColor` | Yes. The crop shrinks to fit |
| Quarter turns and flip | Partly. 90° on the optional rotation slider, no flip | Yes. Animated |
| Touch gestures | Yes. Pan, pinch, twist. The twist reaches the file only with `rotationSlider` | Yes. Pan, pinch, twist, two handles at once |
| Keyboard | Yes. Arrows pan the photo, sliders, buttons | Yes. Crop area, dial, zoom and ratio group; handles no |
| Screen reader | Partly. Dialog and slider roles, sliders have no names | Partly. Roles and live region; you write the labels |
| Image export | Yes. A `File` labelled with the picked file's type | Yes. JPEG, PNG or WebP blob |
| Saved crop | Partly. Reads out a pixel rectangle through `cropperProps` | Yes. Fractions of the photo, restores exactly |
| Ready-made UI | Yes. Antd modal, sliders, OK and Cancel | No. Unstyled 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](https://zriz.dev/docs/zriz-vs-react-easy-crop.md) 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](https://github.com/nanxiaobei/antd-img-crop/blob/9ed87c0c7b5f161c89db82af53e5adb5b77c129c/src/ImgCrop.tsx#L112-L115) 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 `Upload`. Your own dialog with `Cropper.Root` inside, opened from `beforeUpload`
- `aspect`, default 1. `aspect` 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
- `showGrid`. `Cropper.Grid` inside `Area`. It shows while the crop moves and hides at rest
- `zoomSlider`. `Zoom` with `ZoomThumb`, two `ZoomStep` buttons, and `wheel="zoom"` on `Viewport`
- `maxZoom`, default 3. `maxZoom` on `Root`, default 15
- `minZoom`. No equivalent
- `rotationSlider`. `Dial` for 45° each way, `Rotate` for quarter turns
- `aspectSlider`, `minAspect`, `maxAspect`. `aspect` on `Root` from a range input of your own, or `AspectGroup` presets
- `showReset`, `resetText`. `Cropper.Reset` with your text. It also puts the position back
- `quality`, default 0.4. `quality` in `exportCrop`, default 0.9, or the photo's own for a JPEG file when that is lower
- `fillColor`. No equivalent. A JPEG export paints clear pixels white
- `cropperProps`. Props on the zriz parts
- `modalTitle`, `modalWidth`, `modalOk`, `modalCancel`, `modalClassName`, `modalProps`. Props of your own dialog
- `onModalOk`. Your OK handler, after `exportCrop` resolves
- `onModalCancel`. Your cancel handler
- `beforeCrop`. A check of your own before you open the dialog
- The forwarded `ref`. `actionsRef` 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.

```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 { 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](https://github.com/nanxiaobei/antd-img-crop) whose README is its documentation, and an [npm page](https://www.npmjs.com/package/antd-img-crop). The [Ant Design Upload docs](https://ant.design/components/upload) show it in use.

## More comparisons

- [zriz vs react-easy-crop](https://zriz.dev/docs/zriz-vs-react-easy-crop.md). The cropper inside antd-img-crop, used on its own without antd
- [zriz vs Uppy image editor](https://zriz.dev/docs/zriz-vs-uppy-image-editor.md). Another cropper that is a step inside an upload flow
- [zriz vs react-avatar-editor](https://zriz.dev/docs/zriz-vs-react-avatar-editor.md). A small avatar and profile picture component, with no antd

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