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
| 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 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 | NoProps set the frame size | YesFour corners and four edges |
| Photo moves under the frame | Yes | Yes |
| Aspect ratio | PartlyFixed by the width and height props | YesFree, original or any ratio |
| Round crop | PartlyRound mask, rectangular export | YesMask and export |
| Zoom | YesPinch, opt-in wheel, keys; app stores scale | YesPinch, Ctrl + wheel, slider part, keys |
| Straighten (free rotation) | YesAny angle through the rotate prop | Partly45° each way plus quarter turns |
| Crop stays inside the photo at an angle | PartlyPan is limited; low zoom leaves empty corners | YesThe crop shrinks to fit |
| Quarter turns and flip | PartlyTurns through rotate, no flip | YesAnimated |
| Touch gestures | PartlyPan and pinch, no twist | YesPan, pinch, twist, two handles at once |
| Keyboard | YesArrows pan, plus and minus ask for zoom | YesCrop area, dial, zoom and ratio group; handles no |
| Screen reader | PartlyRole and fixed English label, no live region | PartlyRoles and live region; you write the labels |
| Image export | PartlyReturns a canvas, the app encodes it | YesJPEG, PNG or WebP blob |
| Saved crop | PartlyThe app stores position, scale and rotate | YesFractions of the photo, restores exactly |
| Ready-made UI | PartlyCanvas with mask and grid, no controls | NoUnstyled 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>RootwithViewport,Image,MaskandAreainsideimagesrconRoot, a string or aBlobwidth,heightaspectonRootand a CSS size onViewportborderCSSpaddingonViewportborderRadiusat half the widthshape="circle"onRootcolor--zriz-scrimborderColor--zriz-frameshowGrid,gridColorTheGridpart and--zriz-grid. It shows during a gesturescale,onRequestScaleChangeNothing to wire.Zoomfor a slider,maxZoomfor the limit,zoomto control itenableWheelZoomwheel="zoom"onViewportrotatequarterTurnsandrotationin the value.RotateandDialas controlsposition,onPositionChangecenterin the value, withdefaultValueorvalueandonValueChangecrossOrigincrossOriginonRootstylestyleandclassNameon every partonLoadSuccess,onImageReadyonLoadonLoadFailureonError, with a typed erroronImageChange,onMouseMoveonValueChangeonMouseUponValueCommitref,useAvatarEditor()actionsRef,useCropper()getCroppingRect()getValue()for fractions,getPixelCrop().pixelRectfor pixels,nullat an anglegetImage()exportCrop(cropper), a blobgetImageScaledToCanvas()exportCrop(cropper, { maxSize }). It never scales upkeyboardStepNo prop. Arrows move 1% of the crop, 10% with ShiftonLoadStartNo equivalentdisableHiDPIScalingNo prop.previewEdgeonRootcaps the preview sizedisableCanvasRotationNo prop. A quarter turn turns a numeric aspect with itbackgroundColor,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
- zriz vs react-easy-cropAlso moves the photo under a fixed frame, with
zoomandrotationas props - zriz vs CroppieA fixed viewport with a built-in zoom slider, without React
- zriz vs antd-img-cropA ready crop modal for the Ant Design upload
Every comparison is on the comparison page.