# zriz vs react-avatar-editor

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

react-avatar-editor is one React component that draws a fixed crop frame on a canvas and moves the photo under it. zriz is a set of unstyled parts with a resizable crop, a straighten dial and a file export.

We make zriz, so read this page with that in mind. It compares `react-avatar-editor` 16.0.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 react-avatar-editor to zriz. Read https://zriz.dev/docs/zriz-vs-react-avatar-editor.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-avatar-editor describes itself as an "Avatar / profile picture cropping component for React. Resize, crop and rotate your uploaded image using a simple and clean user interface." It is one component that paints the photo and a mask on a `<canvas>`. The frame has the size you pass as props, and the visitor moves the photo under it. Its docs are the [README](https://github.com/mosch/react-avatar-editor#readme).

Pick react-avatar-editor when the job is a fixed-frame avatar picker and weight matters. It adds about 5 kB to the bundle, against 22 to 30 kB for zriz. It has been on npm since 2014 and has over 980,000 downloads a week. Its README names React 17, 18 and 19. It ships a CommonJS build next to ESM, and zriz is ESM only. It can zoom out below the fit and fill the rest with a colour. `rotate` takes any angle, `borderRadius` takes any radius, and `getImage()` returns a canvas synchronously. The canvas comes with a label for screen readers and paints a pulsing fill while a photo loads.

Pick zriz when the visitor resizes the crop or picks a ratio, when a straightened photo must fill the frame, or when the saved file must be round. zriz keeps the zoom itself, takes your labels for screen readers 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

| Fact | react-avatar-editor | zriz |
| - | - | - |
| First release | 2014 | November 2026 |
| npm downloads a week | 980k+ | Under 1,000 |
| GitHub stars | 2.5k+ | Under 100 |
| Licence | MIT | MIT |
| Dependencies | None, React and React DOM are peers | None, `@zriz/core` and React are peers |
| React | 17, 18 and 19 per its README | 18.2 and 19 |
| Size | About 5 kB | 22 to 30 kB |

Size is minified JavaScript and CSS, compressed with Brotli, without React. For react-avatar-editor it is the basic example from its docs, built with esbuild. That already holds `getImage()` and `getImageScaledToCanvas()`, and with a range input, wheel zoom and a `toBlob` save it stays at about 5 kB. react-avatar-editor has no CSS. 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.

The peer range of react-avatar-editor also lists React 0.14 and 16. Its code imports hooks, and its README names 17, 18 and 19.

## Features

| Feature | react-avatar-editor | zriz |
| - | - | - |
| Crop box with handles | No. Props set the frame size | Yes. Four corners and four edges |
| Photo moves under the frame | Yes | Yes |
| Aspect ratio | Partly. Fixed by the `width` and `height` props | Yes. Free, original or any ratio |
| Round crop | Partly. Round mask, rectangular export | Yes. Mask and export |
| Zoom | Yes. Pinch, opt-in wheel, keys; app stores `scale` | Yes. Pinch, Ctrl + wheel, slider part, keys |
| Straighten (free rotation) | Yes. Any angle through the `rotate` prop | Partly. 45° each way plus quarter turns |
| Crop stays inside the photo at an angle | Partly. Pan is limited; low zoom leaves empty corners | Yes. The crop shrinks to fit |
| Quarter turns and flip | Partly. Turns through `rotate`, no flip | Yes. Animated |
| Touch gestures | Partly. Pan and pinch, no twist | Yes. Pan, pinch, twist, two handles at once |
| Keyboard | Yes. Arrows pan, plus and minus ask for zoom | Yes. Crop area, dial, zoom and ratio group; handles no |
| Screen reader | Partly. Role and fixed English label, no live region | Partly. Roles and live region; you write the labels |
| Image export | Partly. Returns a canvas, the app encodes it | Yes. JPEG, PNG or WebP blob |
| Saved crop | Partly. The app stores `position`, `scale` and `rotate` | Yes. Fractions of the photo, restores exactly |
| Ready-made UI | Partly. Canvas with mask and grid, no controls | No. Unstyled parts and an optional stylesheet |

We also ran the rows on touch, keyboard, zoom and export in Chromium. A two-finger twist changed the `scale` request there and left `rotate` at 0.

The flip row rests on a search of the 16.0.0 source for `flip`, `mirror`, `scaleX` and `scale(-1`, which found nothing.

## What you will notice

react-avatar-editor is smaller and has less to wire for a plain avatar picker. One component, no stylesheet, and `getImage()` returns a canvas at source resolution synchronously. zriz needs five parts for the same picker, and `exportCrop` is async.

Zoom is a request in react-avatar-editor. A pinch, the `+` and `-` keys and, with `enableWheelZoom`, the wheel call `onRequestScaleChange` with a number, and nothing moves until the app stores it and passes it back as `scale`. The library floors a request at 0.1 and sets no upper limit, so the app writes that too. See [the handlers at 16.0.0](https://github.com/mosch/react-avatar-editor/blob/v16.0.0/packages/lib/src/index.ts). zriz holds the zoom itself and stops at `maxZoom`, 15 by default.

`rotate` takes any number of degrees, which zriz cannot do with one value. The library does not change the zoom at an angle. Since 16.0.0 it limits the pan and centres the photo when the turned crop does not fit, as its [changelog](https://github.com/mosch/react-avatar-editor/blob/v16.0.0/CHANGELOG.md) says. In Chromium, at `rotate` 15 and `scale` 1 on a 3:2 photo, the canvas from `getImage()` had two transparent corners. In the zriz full editor, with the crop on the whole of the same photo, a 15° turn shrank the crop to 74% of each side and the file had no empty corner. That run used a 3:2 crop and not a square one.

`borderRadius` rounds the hole in the mask. `getImage()` and `getImageScaledToCanvas()` return a rectangle and do not read it, see [the core at 16.0.0](https://github.com/mosch/react-avatar-editor/blob/v16.0.0/packages/core/src/AvatarEditor.ts). The app rounds the result with CSS or on its own canvas. In zriz `shape="circle"` rounds the mask and the exported PNG. A rounded rectangle with a radius between the two is something react-avatar-editor draws and zriz does not.

The canvas comes with a label, `Image editor. Use arrow keys to reposition the image.`, so a screen reader has a name for it with no work from you. The text is fixed English, and the component drops unknown props, so an app cannot pass its own `aria-label`. zriz ships no words: you write each label, in your language. The arrow keys also run the other way. ArrowLeft moves the photo left in react-avatar-editor, and moves the crop left in zriz.

## Migrate from react-avatar-editor

zriz has no equivalent for these: a `scale` below 1 with the whole photo inside the frame on a `backgroundColor`, `disableBoundaryChecks`, a single angle past 45°, a crop with empty corners, a `borderRadius` between square and circle, a synchronous canvas result, a CommonJS build, and React 17. If the project needs one of them, stay.

- `<AvatarEditor>`. `Root` with `Viewport`, `Image`, `Mask` and `Area` inside
- `image`. `src` on `Root`, a string or a `Blob`
- `width`, `height`. `aspect` on `Root` and a CSS size on `Viewport`
- `border`. CSS `padding` on `Viewport`
- `borderRadius` at half the width. `shape="circle"` on `Root`
- `color`. `--zriz-scrim`
- `borderColor`. `--zriz-frame`
- `showGrid`, `gridColor`. The `Grid` part and `--zriz-grid`. It shows during a gesture
- `scale`, `onRequestScaleChange`. Nothing to wire. `Zoom` for a slider, `maxZoom` for the limit, `zoom` to control it
- `enableWheelZoom`. `wheel="zoom"` on `Viewport`
- `rotate`. `quarterTurns` and `rotation` in the value. `Rotate` and `Dial` as controls
- `position`, `onPositionChange`. `center` in the value, with `defaultValue` or `value` and `onValueChange`
- `crossOrigin`. `crossOrigin` on `Root`
- `style`. `style` and `className` on every part
- `onLoadSuccess`, `onImageReady`. `onLoad`
- `onLoadFailure`. `onError`, with a typed error
- `onImageChange`, `onMouseMove`. `onValueChange`
- `onMouseUp`. `onValueCommit`
- `ref`, `useAvatarEditor()`. `actionsRef`, `useCropper()`
- `getCroppingRect()`. `getValue()` for fractions, `getPixelCrop().pixelRect` for pixels, `null` at an angle
- `getImage()`. `exportCrop(cropper)`, a blob
- `getImageScaledToCanvas()`. `exportCrop(cropper, { maxSize })`. It never scales up
- `keyboardStep`. No prop. Arrows move 1% of the crop, 10% with Shift
- `onLoadStart`. No equivalent
- `disableHiDPIScaling`. No prop. `previewEdge` on `Root` caps the preview size
- `disableCanvasRotation`. No prop. A quarter turn turns a numeric aspect with it
- `backgroundColor`, `disableBoundaryChecks`. No equivalent

Saved crops: an app keeps either the three props or the result of `getCroppingRect()`. `position` is the centre of the crop as fractions of the photo, `scale` is 1 at the fit, and `rotate` is degrees clockwise. The rectangle is `{ x, y, width, height }` as fractions of the unturned photo, with the top-left corner as `x` and `y`, and it does not hold the angle. zriz has no importer for either, and we did not check a conversion by running both libraries, so we give none here. The [value](https://zriz.dev/docs/value.md) page defines every field of a zriz `CropValue` if you write one. Test it on crops with a `rotate` other than 0, and expect zriz to shrink or move any crop that had an empty corner.

Steps: install `@zriz/react` and `@zriz/core`. Replace `<AvatarEditor>` with the parts below and move `width / height` to `aspect`. Delete the `scale` state and the `onRequestScaleChange` handler. Replace the slider that set `scale` with `Zoom`. Replace `getImage().toBlob(...)` with `exportCrop`. Move the mask colours from props to CSS variables. Write an `aria-label` for `Area` and `Zoom`. Then remove `react-avatar-editor`.

```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 = {
  image: string | File;
  onSave: (file: Blob) => void;
};

const track = {
  width: 200,
  height: 24,
  backgroundImage:
    "linear-gradient(currentColor, currentColor)",
  backgroundSize: "100% 2px",
  backgroundPosition: "center",
  backgroundRepeat: "no-repeat",
};

const thumb = {
  top: 4,
  width: 16,
  height: 16,
  borderRadius: 8,
  background: "currentColor",
  translate: "-50%",
};

export const AvatarCropper = ({ image, onSave }: Props) => {
  const [ready, setReady] = useState(false);
  const [failed, setFailed] = useState(false);
  const actions = useRef<CropperApi>(null);

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

    try {
      const { blob } = await exportCrop(actions.current, {
        maxSize: { width: 500, height: 500 },
      });

      setFailed(false);
      onSave(blob);
    } catch {
      setFailed(true);
    }
  };

  return (
    <Cropper.Root
      actionsRef={actions}
      src={image}
      aspect={1}
      maxZoom={5}
      onLoad={() => setReady(true)}
      onError={() => setReady(false)}
    >
      <Cropper.Viewport
        wheel="zoom"
        style={{
          boxSizing: "border-box",
          width: 350,
          height: 350,
          padding: 50,
        }}
      >
        <Cropper.Image />
        <Cropper.Mask />
        <Cropper.Area aria-label="Crop area" />
      </Cropper.Viewport>
      <Cropper.Zoom aria-label="Zoom" max={5} style={track}>
        <Cropper.ZoomThumb max={5} style={thumb} />
      </Cropper.Zoom>
      {failed && <p role="alert">This crop did not save.</p>}
      <button type="button" disabled={!ready} onClick={save}>
        Save
      </button>
    </Cropper.Root>
  );
};
```

This is a 250 px square frame with a 50 px border, a zoom slider that ends at 5× and a save button. `Area` has no handles, so the visitor moves and zooms the photo. Alt with an arrow key still resizes the crop. On a touch screen a two-finger twist also straightens the photo, and `rotateGesture={false}` on `Viewport` turns that off. `exportCrop` returns a JPEG at most 500 px wide. For a round avatar set `shape="circle"` on `Root`, and the file becomes a round PNG. Still yours to write: the layout, the look of the button, the file input, the wording of each label and a loading state. react-avatar-editor paints a pulsing fill while a photo loads, and zriz paints nothing for that. The slider styles here are the ones from the zriz docs. 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 script could not measure the empty share of the frame on its canvas, so the 15° result comes from the corners of the exported file.

Sources: the [repository](https://github.com/mosch/react-avatar-editor), the [README](https://github.com/mosch/react-avatar-editor#readme), the [changelog](https://github.com/mosch/react-avatar-editor/blob/v16.0.0/CHANGELOG.md) and the [npm page](https://www.npmjs.com/package/react-avatar-editor).

## More comparisons

- [zriz vs react-easy-crop](https://zriz.dev/docs/zriz-vs-react-easy-crop.md). Also moves the photo under a fixed frame, with `zoom` and `rotation` as props
- [zriz vs Croppie](https://zriz.dev/docs/zriz-vs-croppie.md). A fixed viewport with a built-in zoom slider, without React
- [zriz vs antd-img-crop](https://zriz.dev/docs/zriz-vs-antd-img-crop.md). A ready crop modal for the Ant Design upload

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