Skip to navigation

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

FactCroppiezriz
First release2015November 2026
npm downloads a week120k+Under 1,000
GitHub stars2.6k+Under 100
LicenceMITMIT
DependenciesNoneNone, @zriz/core and React as peers
ReactNot needed, its README lists no React wrapper18.2 and 19
SizeAbout 8 kB22 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

FeatureCroppiezriz
Crop box with handlesPartlyTwo edge handles with enableResizeYesFour corners and four edges
Photo moves under the frameYesYes
Aspect ratioPartlySet once by the frame's width and heightYesFree, original or any ratio
Round cropYesMask and exportYesMask and export
ZoomYesWheel, pinch, built-in slider, keysYesPinch, 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 angleNoIt has no free rotationYesThe crop shrinks to fit
Quarter turns and flipPartlyA rotate() method; flip through bindYesAnimated
Touch gesturesPartlyPan and pinch, no twistYesPan, pinch, twist, two handles at once
KeyboardYesFrame and zoom slider; resize handles noYesCrop area, dial, zoom and ratio group; handles no
Screen readerPartlyThe slider has a name; frame has nonePartlyRoles and live region; you write the labels
Image exportYesPNG, JPEG or WebP; blob, data URL, canvasYesJPEG, PNG or WebP blob
Saved cropYesCorner points in whole image pixelsYesFractions of the photo, restores exactly
Ready-made UIYesStyled frame and zoom sliderNoUnstyled 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)Root with Viewport, Image, Mask and Area. Without React, see vanilla JS
  • viewport: { width, height }aspect on Root. The crop fits inside the padding of Viewport
  • viewport.type: 'circle'shape="circle" on Root
  • boundary: { width, height }CSS width and height of Viewport
  • showZoomerZoom with ZoomThumb, which you style
  • enableZoom: falseNo switch. maxZoom={1} on Root looks closest, and we did not check it
  • mouseWheelZoom: truewheel="zoom" on Viewport
  • mouseWheelZoom: 'ctrl'wheel="none" on Viewport. The zriz default also pans on a plain wheel
  • mouseWheelZoom: falseNo exact equivalent. wheel="none" is closest, and Ctrl + wheel still zooms
  • enforceBoundary: trueAlways on
  • enforceBoundary: falseNo equivalent
  • enableResizeHandle parts, up to eight
  • enableOrientation and rotate(90)Rotate, or rotate90() on the cropper
  • bind({ orientation }) with 2 or 4Flip with axis, or flip() on the cropper. 5 and 7 also need a quarter turn, which we did not check
  • enableExif with exif-jsNot needed for a JPEG. The browser's decoder applies the orientation
  • customClassclassName on any part
  • maxZoommaxZoom on Root, on another scale, or minSize in source pixels
  • minZoomNo equivalent. Croppie uses it only with enforceBoundary: false. zriz does not zoom out below the largest crop that fits
  • bind({ url })src on Root, which also takes a File or a Blob
  • bind({ points })defaultValue or value on Root, converted as below
  • get()getValue(), or getPixelCrop().pixelRect for pixels
  • update event and optiononValueChange during a gesture, onValueCommit once at its end
  • result({ type: 'blob', format, quality })exportCrop(cropper, { type, quality }) from @zriz/core/dom
  • result({ size: 'original' })The default, with no maxSize
  • result({ size: 'viewport' }), size: { width, height }maxSize, which scales down and never up
  • result({ circle })shape in the export options
  • result types 'base64' and 'canvas'Read the blob yourself, for example with FileReader
  • result types 'rawcanvas' and 'html', backgroundColorNo equivalent
  • setZoom(value)setZoom(zoom), on another scale
  • destroy()Unmount the component
  • bind() again after a hidden container showsThe zriz docs say the crop fits again when Viewport changes 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.

Croppie's repo, docs and npm page.

More comparisons

Every comparison is on the comparison page.