Skip to navigation

zriz vs react-avatar-editor

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 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-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.

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

Factreact-avatar-editorzriz
First release2014November 2026
npm downloads a week980k+Under 1,000
GitHub stars2.5k+Under 100
LicenceMITMIT
DependenciesNone, React and React DOM are peersNone, @zriz/core and React are peers
React17, 18 and 19 per its README18.2 and 19
SizeAbout 5 kB22 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 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

Featurereact-avatar-editorzriz
Crop box with handlesNoProps set the frame sizeYesFour corners and four edges
Photo moves under the frameYesYes
Aspect ratioPartlyFixed by the width and height propsYesFree, original or any ratio
Round cropPartlyRound mask, rectangular exportYesMask and export
ZoomYesPinch, opt-in wheel, keys; app stores scaleYesPinch, Ctrl + wheel, slider part, keys
Straighten (free rotation)YesAny angle through the rotate propPartly45° each way plus quarter turns
Crop stays inside the photo at an anglePartlyPan is limited; low zoom leaves empty cornersYesThe crop shrinks to fit
Quarter turns and flipPartlyTurns through rotate, no flipYesAnimated
Touch gesturesPartlyPan and pinch, no twistYesPan, pinch, twist, two handles at once
KeyboardYesArrows pan, plus and minus ask for zoomYesCrop area, dial, zoom and ratio group; handles no
Screen readerPartlyRole and fixed English label, no live regionPartlyRoles and live region; you write the labels
Image exportPartlyReturns a canvas, the app encodes itYesJPEG, PNG or WebP blob
Saved cropPartlyThe app stores position, scale and rotateYesFractions of the photo, restores exactly
Ready-made UIPartlyCanvas with mask and grid, no controlsNoUnstyled 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. 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 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. 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
  • imagesrc on Root, a string or a Blob
  • width, heightaspect on Root and a CSS size on Viewport
  • borderCSS padding on Viewport
  • borderRadius at half the widthshape="circle" on Root
  • color--zriz-scrim
  • borderColor--zriz-frame
  • showGrid, gridColorThe Grid part and --zriz-grid. It shows during a gesture
  • scale, onRequestScaleChangeNothing to wire. Zoom for a slider, maxZoom for the limit, zoom to control it
  • enableWheelZoomwheel="zoom" on Viewport
  • rotatequarterTurns and rotation in the value. Rotate and Dial as controls
  • position, onPositionChangecenter in the value, with defaultValue or value and onValueChange
  • crossOrigincrossOrigin on Root
  • stylestyle and className on every part
  • onLoadSuccess, onImageReadyonLoad
  • onLoadFailureonError, with a typed error
  • onImageChange, onMouseMoveonValueChange
  • onMouseUponValueCommit
  • 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
  • keyboardStepNo prop. Arrows move 1% of the crop, 10% with Shift
  • onLoadStartNo equivalent
  • disableHiDPIScalingNo prop. previewEdge on Root caps the preview size
  • disableCanvasRotationNo prop. A quarter turn turns a numeric aspect with it
  • backgroundColor, disableBoundaryChecksNo 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 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.

"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 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, the README, the changelog and the npm page.

More comparisons

Every comparison is on the comparison page.