Quick start
Go from an empty project to a cropper that hands you the crop.
Set it up with an agent
Paste this prompt into Claude Code, Cursor or any other coding agent. It reads this page and does the steps below for you:
Add zriz to this project. Read https://zriz.dev/docs/quick-start.md and follow it: install @zriz/react and @zriz/core, import its stylesheet and render a cropper with corner handles, the straighten dial and a file picker for the photo. Build it from the zriz parts, not a crop box of your own, and give every part that asks for one an aria-label. When it works, show me where onValueCommit hands back the crop.
Install
pnpm add @zriz/react @zriz/corenpm, Yarn and Bun work too. FAQ lists what zriz needs.
Render a cropper
Save this component in its own file and render <PhotoCropper /> from a page. It loads a sample photo, and the stylesheet gives it the default look:
"use client";
import * as Cropper from "@zriz/react/cropper";
import "@zriz/react/styles.css";
const photo = "https://picsum.photos/id/1015/1600/1200";
export const PhotoCropper = () => {
return (
<Cropper.Root src={photo}>
<Cropper.Viewport
style={{ maxWidth: 400, height: 300, padding: 24 }}
>
<Cropper.Image />
<Cropper.Mask />
<Cropper.Area aria-label="Crop area">
<Cropper.Handle position="top-left" />
<Cropper.Handle position="top-right" />
<Cropper.Handle position="bottom-right" />
<Cropper.Handle position="bottom-left" />
</Cropper.Area>
</Cropper.Viewport>
<Cropper.Dial
aria-label="Straighten"
style={{ maxWidth: 361 }}
>
<Cropper.DialTicks />
</Cropper.Dial>
</Cropper.Root>
);
};Viewport needs a size, and the image and the mask fill it. The 24 px padding leaves room for the handles, see viewport. The cropper now drags, pinches, resizes and straightens:
The demo above runs this component at the zriz site's size, with a public domain photo, open, reset and download buttons and the site's styles. Like the code, it shows no angle above the dial.
Let the user pick a photo
Keep the chosen file in state and pass it to src:
"use client";
import * as Cropper from "@zriz/react/cropper";
import "@zriz/react/styles.css";
import { useState } from "react";
export const PhotoCropper = () => {
const [photo, setPhoto] = useState<File | null>(null);
return (
<>
<input
aria-label="Choose a photo"
type="file"
accept="image/*"
onChange={(e) => setPhoto(e.target.files?.[0] ?? null)}
/>
<Cropper.Root
src={photo}
onError={(error) => console.error(error.code)}
>
<Cropper.Viewport
style={{ maxWidth: 400, height: 300, padding: 24 }}
>
<Cropper.Image />
<Cropper.Mask />
<Cropper.Area aria-label="Crop area">
<Cropper.Handle position="top-left" />
<Cropper.Handle position="top-right" />
<Cropper.Handle position="bottom-right" />
<Cropper.Handle position="bottom-left" />
</Cropper.Area>
</Cropper.Viewport>
<Cropper.Dial
aria-label="Straighten"
style={{ maxWidth: 361 }}
>
<Cropper.DialTicks />
</Cropper.Dial>
</Cropper.Root>
</>
);
};src takes a URL, a File or a Blob. While it is null, the viewport stays empty. When a photo cannot load, onError gets a ZrizError, and errors lists its codes. A URL from another domain needs CORS headers, and a new photo keeps the current crop. Loading images explains both.
Get the crop
Add onValueCommit to Root in the picker above. It fires once when a gesture ends, a key press on the crop or the dial included, or on the frame after a write such as rotate90(). The first photo sends no commit until the user touches it, and a later photo sends one only when it has to refit the crop, see events.
<Cropper.Root
src={photo}
onError={(error) => console.error(error.code)}
onValueCommit={(value, details) => {
console.log(value, details.pixelCrop);
}}
>value is the crop as fractions of the photo, ready to store and load back. Value lists every field. details.pixelCrop is the export plan for that crop in source pixels, or null before a photo loads. Its pixelRect is the crop rectangle, and it is null with a straighten angle or perspective. getPixelCrop() on the cropper from actionsRef reads it for an untouched photo, see hooks. Exporting turns the crop into a file in the browser or on your server.
Playground changes the main props live, and full editor puts every part in one component.