Image Cropper React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
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.
Drag the photo with a finger or the mouse to move it under the frame; pinch with two fingers or turn the mouse wheel to zoom, and the image always keeps covering the frame. While you move it a rule-of-thirds grid fades in and a small pill on the stage shows the zoom and the angle; a first-run hint says how to move and zoom and disappears after the first touch. Pick a ratio (Free, 1:1, 4:3, 3:4, 16:9, 9:16) from the segmented switch on wide layouts or the scrolling chip rail on phones, where every chip is a 44px target. In Free mode the frame starts at the photo's own ratio and its four corner brackets resize it with a drag (the frame stays centered). The zoom row has minus and plus buttons that glide the zoom on a short tween around a slider; the rotation row has a 90° left turn that animates, a slider from −45° to 45° with a center mark for straightening, and an optional horizontal flip that turns like a card. Reset puts position, zoom, rotation and flip back. Tab to the crop frame (it shows a white focus ring): arrow keys move the photo by 10px (Shift+arrows by 2px for fine nudges), + and − zoom, r turns 90° left, f flips when flippable, 0 resets and Escape cancels a corner drag. The sliders take arrows, Home and End, and read their value as 150% or −12°; aspect changes, turns, flips and resets are announced. On phones the stage caps at 60% of the screen height and the toolbar stacks. The export crops in a canvas with the rotation and flip, steps down large photos in halves for a sharp result, and can mask a circle with transparent corners.
Image Cropper is a Pro React component in the React media components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add image-cropper, together with Button, Slider, Segmented Control, Tooltip, Skeleton and 6 more and the npm packages react-easy-crop, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add image-cropperImage Cropper is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import ImageCropper from @/components/ui/image-cropper.
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { ImageCropper, type ImageCropperApi } from "@/components/ui/image-cropper"
export function ListingPhoto({ onSave }: { onSave: (file: Blob) => void }) {
const [api, setApi] = useState<ImageCropperApi | null>(null)
const [saving, setSaving] = useState(false)
const crop = async () => {
if (!api) return
setSaving(true)
try {
onSave(await api.toBlob({ type: "image/jpeg", quality: 0.9, maxSize: 1600 }))
} finally {
setSaving(false)
}
}
return (
<div className="flex max-w-xl flex-col gap-4">
<ImageCropper
src="/demo/apartment.jpg"
defaultAspect={4 / 3}
setApi={setApi}
labels={{
aspect: "Ratio",
hint: "Drag to move, scroll to zoom",
touchHint: "Drag to move, pinch to zoom",
}}
/>
<Button loading={saving} onClick={crop}>
Crop
</Button>
</div>
)
}Props
src*string | Blob | FileThe photo: a URL, a data URL or a File / Blob from an input (its object URL is created and revoked for you). Modern browsers apply EXIF orientation to both the preview and the export.
valueCropValue ({ area: CropArea; zoom: number; rotation: number; aspect: number | null; flipH?: boolean })The controlled crop. area is { x, y, width, height } in source pixels of the rotated photo. A value from outside that differs from the last one reported restores that exact crop.
defaultValueCropValueThe uncontrolled start, for example a crop saved earlier. Without it the frame starts centered at the largest area of the ratio.
onValueChange(value: CropValue) => voidFires when an interaction ends (a drag, a pinch, the wheel settling, a slider released, a key released, a tween finished), never per frame. Also once when the photo is ready, with the starting crop.
onCropChange(value: CropValue) => voidEvery frame while the crop moves. Rarely needed; prefer onValueChange.
aspectnumber | nullThe controlled ratio (width / height). null is a free crop that starts at the photo's own ratio and has corner handles.
defaultAspectdefault"null"number | nullThe uncontrolled starting ratio, e.g. 4 / 3 for a listing photo.
onAspectChange(aspect: number | null) => voidA ratio was picked from the presets.
aspectPresetsdefault"DEFAULT_ASPECT_PRESETS"AspectPreset[] | false ({ key: string; label: ReactNode; value: number | null; icon?: ReactNode })The ratios offered: Free, 1:1, 4:3, 3:4, 16:9, 9:16 by default, each with a small outline of its shape. false hides the picker (a fixed ratio). The default Free label follows labels.free. Exported as DEFAULT_ASPECT_PRESETS, so you can spread it and add your own.
cropShapedefault"rect""rect" | "round"round draws a circular frame for avatars and logos, forces the ratio to 1 and hides the presets; the export masks the circle.
showGriddefault"interacting""always" | "interacting" | "never"The rule-of-thirds grid inside the frame. interacting fades it in while you drag, pinch, zoom or rotate.
minZoomdefault1numberThe smallest zoom. At 1 the photo just fits the stage. With a resized free frame the zoom also stops where the photo would stop covering it.
maxZoomdefault4numberThe largest zoom for the slider, the buttons, the wheel and pinch.
zoomStepdefault0.1numberThe slider step and the + / − key step. The zoom buttons move five steps with a short glide.
rotatabledefaulttruebooleanShows the rotation row: a 90° left turn and a fine slider from −45° to 45° with a center mark.
flippabledefaultfalsebooleanAdds a horizontal flip button (and the f key). The export mirrors the photo too.
restrictPositiondefaulttruebooleanKeeps the photo covering the whole frame. Off, the photo can move past the frame and the uncovered part exports as the background.
objectFitdefault"contain""contain" | "cover"How the photo sits in the stage at zoom 1: contain shows all of it, cover fills the stage.
heightdefault360numberThe stage height in px. Phones cap it at 60dvh so the toolbar stays on screen.
toolbardefaulttruebooleanThe controls under the stage. Off, the photo still moves, pinches, zooms with the wheel and answers the keys; drive the rest through setApi.
exportdefault"{ type: "image/jpeg", quality: 0.9, maxSize: 2048 }"CropExportOptions ({ type?: ImageType; quality?: number; maxSize?: number; width?: number; height?: number; background?: string; round?: boolean })Defaults for toBlob / toCanvas. maxSize caps the longest side (never upscales), width / height set an exact size, round masks a circle (on by default for a round crop), background fills behind (JPEG gets white). cropImage (see Parts) runs the same export without the component.
overlayColordefault"rgba(0, 0, 0, 0.55)"stringThe dim outside the frame. Any CSS color.
sizedefault"md""sm" | "md"The toolbar controls: sm 32px buttons and slim sliders, md 40px (44px on touch).
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The slider fills, the selected ratio chip and the segmented thumb.
colordefault""stringAny CSS color; replaces the tone for the sliders and the selected ratio.
radiusdefault"md""none" | "sm" | "md" | "lg"The stage corners: 0 / 12 / 16 / 24px.
disableddefaultfalsebooleanDims the stage and the toolbar and ignores every gesture and key.
setApi(api: ImageCropperApi) => voidReceives { toBlob(options?), toCanvas(options?), reset(), rotateBy(degrees), zoomTo(zoom) } after mount. toBlob rejects with labels.corsError when a remote photo has no CORS headers (the preview still works).
onError(error: Error) => voidThe photo failed to load, or an export failed. Load errors also show a retry on the stage for URLs.
labelsdefault"DEFAULT_IMAGE_CROPPER_LABELS"Partial<ImageCropperLabels>Every string: aspect, free, zoom, zoomIn, zoomOut, rotate, rotateLeft, flip, reset, area (the frame's name for screen readers), loading, error, retry, hint, touchHint, corsError, exportError, and the announcements aspectChanged(label), rotated(degrees, formatted), flipped, resetDone. English defaults in DEFAULT_IMAGE_CROPPER_LABELS.
classNamesPartial<Record<"stage" | "area" | "grid" | "handle" | "toolbar" | "aspects" | "zoom" | "rotate" | "reset" | "pill", string>>Extra classes for the inner parts.
classNamestringExtra classes for the root, merged last.
Parts
cropImage
functionRuns a saved crop without the component, e.g. on submit or in avatar-upload: rotation and flip aware, two-step downscaling for large photos, and a round mask with transparent PNG or WebP corners. Browser only.
source*string | BlobThe image URL or file.
value*CropValueThe crop from onValueChange: area, zoom, rotation, aspect, flipH.
optionsCropExportOptionstype, quality, maxSize, width, height, background, round.
returnsPromise<Blob>The cropped image, ready to upload.
DEFAULT_ASPECT_PRESETS
constantThe ratios the picker offers: Free, 1:1, 4:3, 3:4, 16:9 and 9:16. Spread it and add your own { key, label, value }.
DEFAULT_IMAGE_CROPPER_LABELS
constantEvery English string: toolbar buttons, hints, loading and error states, and the screen reader announcements. Spread it to translate.
CropValue
typeEverything needed to redo a crop later: { area, zoom, rotation, aspect, flipH? }. Store it, pass it back as value or to cropImage.
ImageCropperApi
typeThe handle setApi hands you: toBlob(options?), toCanvas(options?), reset(), rotateBy(degrees), zoomTo(zoom).
Examples
Cover image
16:9 only, exported as WebP at up to 1920px.
Round logo
cropShape="round" with a 512px PNG export: the corners stay transparent.
Your own photo
A file input hands a File to src; the object URL is created and released for you.
Free aspect
Drag the frame's corners to resize it; the grid stays on.
Scanned document
Custom A4 presets, fine rotation and a flip button.
Phone editor
A 360px screen: stacked toolbar, scrolling ratio chips, pinch to zoom.
Brand tone
tone="brand", size sm, a warmer overlay.
Dependencies
Files
- components/ui/image-cropper.tsx
FAQ
Is the React Image Cropper component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Image Cropper?
Run npx wingo-ui@latest add image-cropper. It also adds the Wingo UI items it builds on (Button, Slider, Segmented Control, Tooltip, Skeleton and 6 more) and the npm packages react-easy-crop, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/image-cropper.
How does Image Cropper behave on phones?
Pick a ratio (Free, 1:1, 4:3, 3:4, 16:9, 9:16) from the segmented switch on wide layouts or the scrolling chip rail on phones, where every chip is a 44px target. On phones the stage caps at 60% of the screen height and the toolbar stacks.
See also
Used in
More React media components
Articles using this component
- React Image Cropper: Crop Before Upload, Zoom, Rotate
Component guides9 min read
- React File Upload: Drag and Drop, Paste, Camera, Progress
Component guides11 min read
