Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelog
  1. Home
  2. Blog
  3. Component guides
  4. React Image Cropper: Crop Before Upload, Zoom, Rotate
Auf Deutsch lesen
  1. Blog
  2. Component guides
  3. React Image Cropper: Crop Before Upload, Zoom, Rotate
Component guides
Component guides

React Image Cropper: Crop Before Upload, Zoom, Rotate

SR
Serban Rusu · Founder of Wingo UI
Oct 19, 2026 · 9 min read

On this page

0%
  1. What should a React image cropper handle?
  2. Which React image crop library should I use?
  3. How do I crop an image before upload in React?
  4. How do I export a sized WebP from the crop?
  5. How do I make a React image crop circle?
  6. How do I build an avatar upload crop React flow?
  7. How does cropping work on phones and with a keyboard?
  8. What are the common image cropper pitfalls?
  9. How do I add it to my project?
  10. FAQ
    1. What is the best React image crop library?
    2. How do I crop an image to a circle in React?
    3. Should I crop images on the client or the server?
    4. Why does my cropped image fail to export from a remote URL?
    5. Is the Wingo UI Image Cropper free?

TL;DR

A React image cropper should crop in the browser before the upload: move with a drag, zoom with a pinch or the wheel, straighten with rotation, then export a resized WebP or JPEG Blob through a canvas so a 12 megapixel phone photo never travels at full size. For avatars, use a round frame and export a PNG or WebP with transparent corners. The Wingo UI Image Cropper does this with a setApi handle and a cropImage helper, and Avatar Upload wraps it into a pick, crop and upload flow with a progress ring.

Published Oct 19, 2026

You let people upload a profile photo or a listing picture, and the photos arrive as 4,000 pixel JPEGs, sideways faces and heads cut off by object-fit: cover. A React image cropper fixes this before the file leaves the browser: the user frames the photo, you export a small image at the exact size you need, and the server stores what the page will show. This guide covers what a cropper must handle, how the common libraries compare, how to crop before upload with the Wingo UI Image Cropper, and the round avatar flow built on it.

What should a React image cropper handle?

A cropper that holds up in production handles seven things:

  1. Pan and zoom with a mouse drag, the wheel, a one finger drag and a two finger pinch, while the photo always covers the frame.
  2. Aspect ratios: a fixed one for covers and avatars, presets for galleries, and a free crop with resizable corners.
  3. Rotation: 90 degree turns for sideways photos and a fine slider to straighten a horizon.
  4. A sized export: an exact width or a cap on the longest side, in WebP, JPEG or PNG, as a Blob you can put in FormData.
  5. A round frame for avatars and logos, with transparent corners in the export.
  6. Keyboard support: arrows to move, keys to zoom and rotate, and sliders that read their value.
  7. A phone layout where the toolbar stays on screen and every control is a 44px target.

Point 4 is where hand-rolled croppers usually break. Drawing a large photo straight down to 512px in one drawImage call can leave jagged edges, and a canvas past the browser's size limit can fail or come back blank.

Which React image crop library should I use?

These are the names that come up when you search for a React image crop library. Facts are from each project's README on GitHub, as of October 2026.

OptionLicenseWhat you getZoom and rotationRound cropExport
react-easy-crop (opens in a new tab)MITA fixed frame, the photo moves under itzoom (maxZoom defaults to 3), wheel zoom, rotationcropShape="round"Pixel coordinates through onCropComplete; the canvas step is yours
react-image-crop (opens in a new tab)ISCA crop box you drag and resize over a fixed image, no dependencies, under 5KB gzipNot in the component; the README's helpers take optional scale and rotatecircularCropCrop coordinates; the README's FAQ shows cropToCanvas and cropToImg helpers
Wingo UI Image CropperPro licensereact-easy-crop plus a toolbar: ratio presets, zoom and rotation sliders, flip, free crop cornersPinch, wheel, buttons, maxZoom 4, 90 degree turns and a fine slider from −45 to 45 degreescropShape="round" with a masked exporttoBlob, toCanvas and cropImage with size, type, quality and mask

react-image-crop is the better choice when users should draw a box over a document or a screenshot, and when bundle size matters most. react-easy-crop fits photos, where moving the picture under a fixed frame feels more natural on a phone. The Wingo UI component is react-easy-crop with the UI and the export already written, as source you can edit.

How do I crop an image before upload in React?

Put the cropper between the file input and the upload. The Image Cropper takes a File as src (it creates and revokes the object URL for you), and setApi hands you a handle with toBlob. A crop before upload React flow needs three pieces: a file input, the cropper and the upload call.

bash
npx wingo-ui@latest add image-cropper
tsx
"use client";
import { useRef, useState, type ChangeEvent } from "react";
import { Button } from "@/components/ui/button";
import { ImageCropper, type ImageCropperApi } from "@/components/ui/image-cropper";
export function CoverPhotoField() {
const [file, setFile] = useState<File | null>(null);
const [api, setApi] = useState<ImageCropperApi | null>(null);
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const onPick = (event: ChangeEvent<HTMLInputElement>) => {
const picked = event.target.files?.[0];
if (picked) setFile(picked);
// the same file can be picked again
event.target.value = "";
};
const save = async () => {
if (!api) return;
setSaving(true);
setError("");
try {
const blob = await api.toBlob({ type: "image/webp", quality: 0.85, maxSize: 1600 });
const body = new FormData();
body.append("cover", blob, "cover.webp");
const response = await fetch("/api/listings/cover", { method: "POST", body });
if (!response.ok) throw new Error("The upload failed");
} catch (failure) {
setError(failure instanceof Error ? failure.message : "The upload failed");
} finally {
setSaving(false);
}
};
return (
<div className="flex max-w-xl flex-col gap-4">
<input ref={inputRef} type="file" accept="image/jpeg,image/png,image/webp" className="sr-only" tabIndex={-1} onChange={onPick} />
{file && <ImageCropper src={file} defaultAspect={16 / 9} setApi={setApi} />}
<div className="flex gap-2">
<Button variant="soft" onClick={() => inputRef.current?.click()}>
Choose a photo
</Button>
<Button loading={saving} disabled={!file} onClick={save}>
Save cover
</Button>
</div>
{error && <p role="alert" className="text-sm text-destructive-soft-foreground">{error}</p>}
</div>
);
}

defaultAspect sets the starting ratio and leaves the presets (Free, 1:1, 4:3, 3:4, 16:9, 9:16) available; pass aspectPresets={false} to lock it. onValueChange fires when an interaction ends, never per frame, with a CropValue: the area in source pixels, the zoom, the rotation, the aspect and the flip.

Drag the photo, pinch or scroll to zoom, turn it with the rotation slider, then press Crop to see the exported size and weight
$ npx wingo-ui@latest add image-cropper
ProImage Cropper docs

How do I export a sized WebP from the crop?

Pass export options to toBlob, or set them once with the export prop. maxSize caps the longest side and never upscales, width and height set an exact size, type picks image/webp, image/jpeg or image/png, and quality (0 to 1) applies to WebP and JPEG. Without options the export is a JPEG at 90% quality, capped at 2048px.

Three details of the export are worth knowing before you write your own:

  • Stepped downscaling. The crop is first drawn at a power-of-two multiple of the final size (never above the source resolution), then halved step by step while the next half is still at least the target, and only then drawn at the final size. A large phone photo exported at 512px stays sharp instead of aliased.
  • A canvas cap. The working canvas never exceeds 16,777,216 pixels (4096 by 4096), so a huge photo is scaled down instead of producing a blank export.
  • No metadata. The file is re-encoded from a canvas, so it carries no EXIF data, which also drops the GPS location a phone writes into photos. Current browsers apply the EXIF orientation when they draw the photo (image-orientation (opens in a new tab) defaults to from-image), so the preview and the export match.

JPEG has no transparency, so the export fills the background white unless you pass background. If a browser cannot encode the type you ask for, canvas toBlob (opens in a new tab) falls back to PNG, so read blob.type before you name the file.

How do I make a React image crop circle?

Set cropShape="round". The frame becomes a circle, the ratio is forced to 1, the presets hide, and the export masks the circle by default. Ask for PNG or WebP to keep the corners transparent:

tsx
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { cropImage, ImageCropper, type CropValue } from "@/components/ui/image-cropper";
export function LogoCrop({ file, onDone }: { file: File; onDone: (blob: Blob) => void }) {
const [crop, setCrop] = useState<CropValue | null>(null);
const save = async () => {
if (!crop) return;
onDone(await cropImage(file, crop, { type: "image/png", width: 512, height: 512, round: true }));
};
return (
<div className="flex flex-col gap-4">
<ImageCropper src={file} cropShape="round" rotatable={false} onValueChange={setCrop} />
<Button onClick={save} disabled={!crop}>
Save logo
</Button>
</div>
);
}

cropImage runs the same export without the component, from the original file and a saved CropValue. That is the pattern to use when you want to re-crop later: store the original upload and the crop value, pass the value back as defaultValue when the user edits again, and regenerate any size you need.

The round frame forces a 1:1 crop. Move the photo inside the circle, then press Crop: the PNG keeps transparent corners on the checkerboard
$ npx wingo-ui@latest add image-cropper
ProImage Cropper docs

How do I build an avatar upload crop React flow?

Use Avatar Upload, which puts the cropper inside the whole profile photo flow. Tap the avatar to open a menu (Choose a photo, Take photo, and Remove photo when you pass onRemove), crop in a round frame in a dialog, and Save exports a square WebP at 512px. The new picture shows at once while a ring around the Avatar fills with the upload progress. A failed upload brings the old picture back and offers Try again.

bash
npx wingo-ui@latest add avatar-upload
tsx
"use client";
import { useState } from "react";
import { AvatarUpload } from "@/components/ui/avatar-upload";
export function ProfilePhoto({ initial }: { initial: string | null }) {
const [avatar, setAvatar] = useState<string | null>(initial);
return (
<AvatarUpload
name="Emma Carter"
value={avatar}
onValueChange={setAvatar}
outputSize={512}
outputType="image/webp"
onUpload={(blob, { signal, onProgress }) =>
new Promise<string>((resolve, reject) => {
const request = new XMLHttpRequest();
// fetch has no upload progress, XMLHttpRequest does
request.upload.onprogress = (event) => {
if (event.lengthComputable) onProgress(event.loaded / event.total);
};
request.onload = () =>
request.status < 300
? resolve((JSON.parse(request.responseText) as { url: string }).url)
: reject(new Error("The upload failed"));
request.onerror = () => reject(new Error("The connection dropped"));
request.onabort = () => reject(new DOMException("Aborted", "AbortError"));
signal.addEventListener("abort", () => request.abort());
const body = new FormData();
body.append("avatar", blob, "avatar.webp");
request.open("POST", "/api/profile/avatar");
request.send(body);
})
}
onRemove={() => fetch("/api/profile/avatar", { method: "DELETE" })}
/>
);
}

Resolve with the stored URL and it becomes the value. Without onProgress calls the ring spins instead of filling. The signal aborts when the component unmounts or a newer upload starts. variant="row" adds Change photo and Remove buttons beside the avatar for a settings list, and variant="dropzone" is a dashed tile for company logos. crop={false} skips the dialog and uploads the largest centered square. Pass cropperProps to tune the crop step, for example { flippable: true }.

Tap the avatar, choose Choose a photo, crop it in the round frame and press Save: the ring fills while the upload runs
$ npx wingo-ui@latest add avatar-upload
ProAvatar Upload docs

How does cropping work on phones and with a keyboard?

On a phone the photo moves with one finger and zooms with a pinch, the stage caps at 60% of the screen height so the toolbar stays visible, the toolbar stacks, and the ratio presets become a scrolling chip rail with 44px targets. In Avatar Upload the source menu, the crop step and the camera turn into bottom sheets below 768px (responsive, on by default), the same rule as in the responsive dialog on mobile guide. Take photo opens the front camera through capture="user" on phones and a live camera dialog on desktops.

With a keyboard, Tab to the crop frame: arrow keys move the photo by 10px (Shift plus arrows by 2px), + and - zoom, r turns 90 degrees left, f flips when flippable is on, and 0 resets. The zoom and rotation sliders read their values as 150% or −12 degrees, and ratio changes, turns and resets are announced. Every string, announcements included, lives in labels and merges over DEFAULT_IMAGE_CROPPER_LABELS.

What are the common image cropper pitfalls?

  • Remote images without CORS. The preview works, but toBlob rejects with labels.corsError because the canvas is tainted. Crop the File from the input, or serve the image with CORS headers.
  • iPhone HEIC photos. Avatar Upload accepts JPEG, PNG and WebP by default. Safari converts HEIC to JPEG on pick when JPEG is in accept; in other browsers a HEIC file fails the type check and the user sees labels.wrongType.
  • Uploading the original. If you send the picked file and crop on display with CSS, every visitor downloads the full photo. Send the export.
  • Trusting the client. The browser export is a convenience. Check type, size and dimensions again on the server.
  • Listening to every frame. onCropChange fires on every frame while the crop moves. Save on onValueChange, which fires once per interaction.

For the picker side (drag and drop, paste, type and size checks, an upload queue), the React file upload guide pairs the File Upload with this cropper, and the React settings page tutorial shows where a profile photo row sits in a full settings screen.

How do I add it to my project?

Run npx wingo-ui@latest add image-cropper or npx wingo-ui@latest add avatar-upload. The CLI copies the source into components/ui, adds the components and helpers they import, and installs the npm packages they need (react-easy-crop, motion and lucide-react for the cropper). Both are Pro components (pricing). More single component deep dives live in the component guides category, and the React form components guide covers the label, error and touch rules every field in a profile form shares.

Components in this post

  • Image Cropper

    A React image cropper for photos before upload: aspect presets, pinch or wheel zoom, rotation, a thirds grid and a JPEG, PNG or WebP export.

    Pro
  • Avatar Upload

    A React avatar upload for profile pictures: pick or shoot a photo, crop it in a round frame and watch a ring fill while it uploads.

    Pro

FAQ

What is the best React image crop library?

react-easy-crop is the usual pick when you want pan, pinch zoom and rotation, and react-image-crop when you want a small, dependency free crop box you resize by its edges. Both report crop coordinates and leave the final image to you or a helper; a source component such as the Wingo UI Image Cropper adds the toolbar and the export on top of react-easy-crop.

How do I crop an image to a circle in React?

Draw a round frame for the user (cropShape="round" in react-easy-crop and the Wingo UI Image Cropper, circularCrop in react-image-crop), then mask the export with a circular clip on the canvas. Export PNG or WebP to keep the corners transparent, because JPEG has no alpha channel.

Should I crop images on the client or the server?

Crop on the client so the user sees the result and the upload is small, then validate type, size and dimensions again on the server. Keeping the original file and the crop coordinates also lets you re-crop later without asking the user again.

Why does my cropped image fail to export from a remote URL?

Drawing a cross-origin image without CORS headers taints the canvas, and toBlob then throws a SecurityError. Serve the image with Access-Control-Allow-Origin, proxy it through your own domain, or crop the File from the input instead of a URL.

Is the Wingo UI Image Cropper free?

No. The Image Cropper and Avatar Upload are Pro components. The Avatar component that Avatar Upload shows the picture in is free.

  • Image cropper
  • File upload
  • Avatar
  • Mobile UI
  • React

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Component guides

React File Upload: Drag and Drop, Paste, Camera, Progress

Build a React file upload that takes drag and drop, paste and the phone camera, checks type and size, and shows a preview and progress bar for every file.

SRSerban Rusu·Oct 16, 2026·11 min read
Component guides

Embla Carousel React: Accessible Slides, No Layout Shift

Build an embla carousel react component for photos, listings, testimonials and onboarding, with keyboard controls, pausable autoplay and no layout shift.

SRSerban Rusu·Oct 19, 2026·9 min read
Component guides

React Animated Number Counter: Rolling Odometer Digits

Build a React animated number counter that rolls each digit like an odometer, takes its separators from Intl, holds its width and respects reduced motion.

SRSerban Rusu·Oct 19, 2026·9 min read
Newsletter

Get new posts by email

New guides, tutorials and comparisons from the Wingo UI blog, sent when they are published.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • Theme generator

Resources

  • Docs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI