zriz vs Croppie
Croppie is a plain JavaScript cropper with an optional jQuery plugin: a fixed frame, a photo that moves under it and a built-in zoom slider. zriz is a set of unstyled React parts with resize handles and a straighten dial.
We make zriz, so read this page with that in mind. It compares croppie 2.6.5 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 croppie to zriz. Read https://zriz.dev/docs/zriz-vs-croppie.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
Croppie is a plain JavaScript class, new Croppie(element, options), with an optional jQuery plugin. Its docs call it "a fast, easy to use image cropping plugin with tons of configuration options". A frame of fixed pixel size sits in the middle of a box, the photo moves and zooms under it, and a styled zoom slider sits below.
Pick Croppie when the page has no React or no bundler, or runs on jQuery. It is one script and one stylesheet, about 8 kB together as measured below, and it loads from a CDN. It paints its own frame and zoom slider, so a square or round avatar cropper needs no CSS from you. It has been on npm since 2015 and has over 120k downloads a week. It returns a data URL, a canvas or an exact output size, and with enforceBoundary: false the crop can run past the photo. Its docs list browsers back to IE 10 and Safari 4.
Pick zriz when the app is React and the user needs to resize or straighten the crop. It is a set of React parts with bundled types, it renders on the server, and src takes a File or a Blob. It has eight resize handles, ratios you change at runtime, a dial that turns the photo up to 45° each way, and rotate and flip buttons. zriz is new. Its first release was in November 2026, and it has one maintainer, versions at 0.x and under a thousand downloads a week.
Numbers
| Fact | Croppie | zriz |
|---|---|---|
| First release | 2015 | November 2026 |
| npm downloads a week | 120k+ | Under 1,000 |
| GitHub stars | 2.6k+ | Under 100 |
| Licence | MIT | MIT |
| Dependencies | None | None, @zriz/core and React as peers |
| React | Not needed, its README lists no React wrapper | 18.2 and 19 |
| Size | About 8 kB | 22 to 30 kB |
Size is minified JavaScript and CSS, compressed with Brotli, without React. For Croppie it is the basic example from its docs, built with esbuild. Croppie's one script holds its export code, so its number includes export. 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
| Feature | Croppie | zriz |
|---|---|---|
| Crop box with handles | PartlyTwo edge handles with enableResize | YesFour corners and four edges |
| Photo moves under the frame | Yes | Yes |
| Aspect ratio | PartlySet once by the frame's width and height | YesFree, original or any ratio |
| Round crop | YesMask and export | YesMask and export |
| Zoom | YesWheel, pinch, built-in slider, keys | YesPinch, Ctrl + wheel, slider part, keys |
| Straighten (free rotation) | Norotate() takes multiples of 90° | Partly45° each way plus quarter turns |
| Crop stays inside the photo at an angle | NoIt has no free rotation | YesThe crop shrinks to fit |
| Quarter turns and flip | PartlyA rotate() method; flip through bind | YesAnimated |
| Touch gestures | PartlyPan and pinch, no twist | YesPan, pinch, twist, two handles at once |
| Keyboard | YesFrame and zoom slider; resize handles no | YesCrop area, dial, zoom and ratio group; handles no |
| Screen reader | PartlyThe slider has a name; frame has none | PartlyRoles and live region; you write the labels |
| Image export | YesPNG, JPEG or WebP; blob, data URL, canvas | YesJPEG, PNG or WebP blob |
| Saved crop | YesCorner points in whole image pixels | YesFractions of the photo, restores exactly |
| Ready-made UI | YesStyled frame and zoom slider | NoUnstyled parts and an optional stylesheet |
Croppie's two handles sit on the right and bottom edges and are off by default. Each one changes one dimension, and nothing locks the ratio. The frame's size comes from viewport.width and viewport.height in the constructor, and no public method changes it later.
With enforceBoundary on, which is Croppie's default, the photo covered the whole frame in Chromium. A restore through bind({ points }) takes its scale from the frame's width and the width of the points, and does not use their height. The frame needs the width to height ratio it had when you saved. The restore is exact to one image pixel because get() rounds.
What you will notice
Croppie works once its stylesheet is on the page. The frame, the dimmed surround and the zoom slider come painted, and the slider carries aria-label="zoom". In zriz the Zoom slider and every button paint nothing until you style them, and you write every label. axe found no issues in either demo in Chromium.
A plain mouse wheel zooms in Croppie by default, and mouseWheelZoom: 'ctrl' limits that to Ctrl + wheel and leaves a plain wheel to the page. In zriz a plain wheel pans the photo and Ctrl + wheel zooms. wheel="zoom" on Viewport gives you Croppie's default, and wheel="none" gives you its 'ctrl' mode.
Croppie turns the photo in 90° steps through rotate(). That method needs enableOrientation: true and throws without it. Croppie ships no rotate or flip control, and a mirror goes through bind with an EXIF orientation number. zriz has Rotate and Flip parts and a Dial that straightens up to 45° each way. At an angle zriz shrinks the crop, so the saved value and the exported file hold no empty corner.
Both take keys. In Croppie the frame is a tab stop, and each arrow press moves the photo 1 CSS px. Shift + Up and Shift + Down change the zoom by 0.0001 per press. The frame has no role and no name, and croppie.css removes the slider's focus outline and gives it a 1 px white one in Firefox. In zriz the arrows move the crop by 1% of its size, or 10% with Shift. The crop area has role="application", and you write its name. The zriz handles take no keys, and Alt + arrows on the crop area resize it about its centre.
Croppie's latest release, 2.6.5, came out in June 2020, and its repo has over 250 open issues. One of its two authors wrote about the project's status in issue 716. zriz had its first release in November 2026 and has one maintainer. It has under a thousand downloads a week, where Croppie has over 120k.
Migrate from Croppie
zriz has no equivalent for a script tag or a jQuery plugin. It ships ES modules, with no UMD or CommonJS build. It has no enforceBoundary: false, so a crop cannot run past the photo or take a background colour. exportCrop resolves a blob and has no data URL, canvas or HTML result, and its maxSize scales down and never up. zriz has no frame option in screen pixels. You size the frame with CSS on Viewport, and the export does not follow that size. Croppie's docs list Firefox 10, Chrome 12, IE 10, Edge, Safari 4 and Opera 15 and newer, plus iOS and Android. zriz lists the latest two versions of Chrome, Edge and Firefox, Safari and iOS Safari 18 and newer, and Chrome and Samsung Internet on Android.
new Croppie(el, options),$(el).croppie(options)RootwithViewport,Image,MaskandArea. Without React, see vanilla JSviewport: { width, height }aspectonRoot. The crop fits inside the padding ofViewportviewport.type: 'circle'shape="circle"onRootboundary: { width, height }CSS width and height ofViewportshowZoomerZoomwithZoomThumb, which you styleenableZoom: falseNo switch.maxZoom={1}onRootlooks closest, and we did not check itmouseWheelZoom: truewheel="zoom"onViewportmouseWheelZoom: 'ctrl'wheel="none"onViewport. The zriz default also pans on a plain wheelmouseWheelZoom: falseNo exact equivalent.wheel="none"is closest, and Ctrl + wheel still zoomsenforceBoundary: trueAlways onenforceBoundary: falseNo equivalentenableResizeHandleparts, up to eightenableOrientationandrotate(90)Rotate, orrotate90()on the cropperbind({ orientation })with 2 or 4Flipwithaxis, orflip()on the cropper. 5 and 7 also need a quarter turn, which we did not checkenableExifwith exif-jsNot needed for a JPEG. The browser's decoder applies the orientationcustomClassclassNameon any partmaxZoommaxZoomonRoot, on another scale, orminSizein source pixelsminZoomNo equivalent. Croppie uses it only withenforceBoundary: false. zriz does not zoom out below the largest crop that fitsbind({ url })srconRoot, which also takes aFileor aBlobbind({ points })defaultValueorvalueonRoot, converted as belowget()getValue(), orgetPixelCrop().pixelRectfor pixelsupdateevent and optiononValueChangeduring a gesture,onValueCommitonce at its endresult({ type: 'blob', format, quality })exportCrop(cropper, { type, quality })from@zriz/core/domresult({ size: 'original' })The default, with nomaxSizeresult({ size: 'viewport' }),size: { width, height }maxSize, which scales down and never upresult({ circle })shapein the export optionsresulttypes'base64'and'canvas'Read the blob yourself, for example withFileReaderresulttypes'rawcanvas'and'html',backgroundColorNo equivalentsetZoom(value)setZoom(zoom), on another scaledestroy()Unmount the componentbind()again after a hidden container showsThe zriz docs say the crop fits again whenViewportchanges size. We did not check a hidden container
The two zoom scales differ. In Croppie 1 is one image pixel per CSS pixel. In zriz 1 is the largest crop that fits. From orientation 1, Croppie's rotate(90) gives EXIF orientation 8. Its docs call 8 counter-clockwise, and the same docs label the rotate(90) button "Rotate Right". Rotate in zriz turns counter-clockwise by default, and direction="cw" turns it the other way. We did not measure Croppie's direction on screen, so check it in your app.
Croppie saves points as four whole-number strings, [x1, y1, x2, y2], in pixels of the photo from its top-left corner. zriz has no importer for them. For a crop saved with orientation 1, turn the strings into numbers, take the photo's width W and height H, and build a CropValue: center.x is (x1 + x2) / 2 / W, center.y is (y1 + y2) / 2 / H, size.width is (x2 - x1) / W and size.height is (y2 - y1) / H. Pass it as defaultValue. We checked the zriz half of that formula by running code. The Croppie half comes from its source and one crop we measured. We checked nothing for the other seven orientations or for crops saved with relative: true, so test those against pixels before you convert them. Croppie's saved zoom is not needed, see value.
Run pnpm add @zriz/react @zriz/core. Replace the element, the effect that calls new Croppie and destroy(), and the bind call with the component below. Convert saved points as above. Replace each result() call with exportCrop. Then remove croppie, @types/croppie, the croppie.css import and, if nothing else uses it, exif-js.
"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 Source = string | Blob;
type Props = { src: Source; onSave: (png: Blob) => void };
const frame = { width: 200, height: 200 };
const unsaved = "This crop did not save. Try again.";
export const PhotoCropper = ({ src, onSave }: Props) => {
const cropper = useRef<CropperApi>(null);
const running = useRef(false);
const [loaded, setLoaded] = useState<Source | null>(null);
const [failed, setFailed] = useState(false);
const save = async () => {
if (!cropper.current || running.current) return;
running.current = true;
try {
const { blob } = await exportCrop(cropper.current, {
type: "image/png",
maxSize: frame,
});
setFailed(false);
onSave(blob);
} catch {
setFailed(true);
} finally {
running.current = false;
}
};
return (
<>
<Cropper.Root
src={src}
aspect={1}
actionsRef={cropper}
onLoad={() => setLoaded(src)}
>
<Cropper.Viewport
wheel="zoom"
style={{
boxSizing: "border-box",
width: 300,
height: 300,
padding: 50,
}}
>
<Cropper.Image />
<Cropper.Mask />
<Cropper.Area aria-label="Crop area" />
</Cropper.Viewport>
<Cropper.Zoom
aria-label="Zoom"
style={{ width: 300, height: 24 }}
>
<Cropper.ZoomThumb />
</Cropper.Zoom>
</Cropper.Root>
{failed && <p role="alert">{unsaved}</p>}
<button
type="button"
disabled={loaded !== src}
onClick={save}
>
Save
</button>
</>
);
};The component keeps Croppie's model: a square frame of 200 px in a 300 px box, a photo that moves under it, a zoom slider and a wheel that zooms. src takes the URL you passed to bind, or the File itself. Croppie's default result is a PNG data URL at the frame's size on screen, border included, so save asks for a PNG blob of at most 200 by 200 px. Without maxSize the file has the crop's size in the photo's own pixels, up to the export size limit on the loading images page, and without type it is a JPEG.
You still write the styles for the zoom track, its thumb and the save button, and you write the labels in your own language. The export runs in a worker by default and on the main thread where workers or OffscreenCanvas are missing or blocked. For a round crop, add shape="circle" to Root, see avatar.
How we checked
We installed the published npm package at the version above, read its source and docs, and took the counts from the npm registry and GitHub. 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 15° turn ran for zriz and not for Croppie, which has no free rotation.
More comparisons
- zriz vs react-avatar-editorA React component for avatars and profile pictures
- zriz vs react-easy-cropThe most downloaded cropper for React, over 5 million a week
- zriz vs Cropper.jsAnother plain JavaScript cropper, with over 13,000 GitHub stars
Every comparison is on the comparison page.