@ashutoshmishr0/image-cropper
A beautiful, zero-dependency React image cropper modal. The user clicks a trigger, picks an image file, and a smooth modal opens with a full crop interface — drag to pan, zoom slider, resize handles, and shape masks (rectangle, square, circle). On confirm, the cropped image is returned as a base64 PNG string.
Works with Next.js App Router, Pages Router, and any React application.
Installation
npm install @ashutoshmishr0/image-cropper
How It Works
The component handles everything internally — you don't manage the file picker yourself:
- Wrap any element (or use the built-in default button) as
children - User clicks it — a file picker opens automatically
- User selects an image — the crop modal opens
- User drags, zooms, and resizes the crop area
- User clicks "Crop & Apply" —
onCropfires with the base64 PNG - Modal closes — do whatever you want with the result (preview, upload, etc.)
Important: Do not pass your own
<input type="file">or animageprop. The component manages file selection internally — you only needonCrop.
Basic Usage
'use client';
import { useState } from 'react';
import ImageCropper from '@ashutoshmishr0/image-cropper';
export default function Page() {
const [preview, setPreview] = useState<string | null>(null);
return (
<div>
<ImageCropper onCrop={(base64) => setPreview(base64)} />
{preview && <img src={preview} alt="Cropped" />}
</div>
);
}
Custom Trigger Button
Pass any element as children. Clicking it opens the file picker.
<ImageCropper onCrop={(base64) => setPreview(base64)}>
<button className="your-own-button">Upload Photo</button>
</ImageCropper>
Shapes
The shape prop controls the crop mask. There are three options:
| Shape | Value | Ratio Behavior |
|---|---|---|
| Rectangle (default) | "rect" |
Free ratio, or locked via aspectRatio / outputWidth+outputHeight |
| Square | "square" |
Always locked to 1:1 |
| Circle | "circle" |
Always locked to 1:1 (output is a transparent-background circular PNG) |
Rectangle — free crop (no fixed ratio)
<ImageCropper
shape="rect"
onCrop={(base64, cropArea) => {
console.log(base64);
console.log(cropArea); // { x, y, width, height } in natural image pixels
}}
/>
Rectangle — fixed aspect ratio (e.g. 16:9 banner)
<ImageCropper
shape="rect"
aspectRatio={16 / 9}
outputWidth={1200}
outputHeight={675}
onCrop={(base64) => setBanner(base64)}
/>
Square crop
<ImageCropper
shape="square"
outputWidth={500}
outputHeight={500}
onCrop={(base64) => setImage(base64)}
/>
Circle crop (e.g. avatar)
<ImageCropper
shape="circle"
outputWidth={200}
outputHeight={200}
onCrop={(base64) => setAvatar(base64)}
>
<img
src={avatar || '/default-avatar.png'}
style={{ width: 80, height: 80, borderRadius: '50%', cursor: 'pointer' }}
alt="Click to change avatar"
/>
</ImageCropper>
When
shapeis"square"or"circle", the crop ratio is automatically locked to 1:1 — you don't need to also passaspectRatio.
Fixed Pixel Output
Pass outputWidth and outputHeight together to force the exported image to that exact pixel size, regardless of how the crop box is resized on screen. This also locks the crop box's ratio to outputWidth / outputHeight.
<ImageCropper
shape="rect"
outputWidth={800}
outputHeight={400}
onCrop={(base64) => setImage(base64)}
/>
If outputWidth/outputHeight are omitted, the exported image size matches whatever the user resized the crop box to.
Themes
The theme prop switches between a dark modal (default) and a light modal.
<ImageCropper theme="dark" onCrop={(base64) => setImage(base64)} />
<ImageCropper theme="light" onCrop={(base64) => setImage(base64)} />
Opening Programmatically (via ref)
'use client';
import { useRef } from 'react';
import ImageCropper, { type ImageCropperRef } from '@ashutoshmishr0/image-cropper';
export default function Page() {
const cropperRef = useRef<ImageCropperRef>(null);
return (
<>
<button onClick={() => cropperRef.current?.open()}>
Open Cropper
</button>
<ImageCropper
ref={cropperRef}
onCrop={(base64) => console.log(base64)}
/>
</>
);
}
Uploading the Cropped Image to a Server
<ImageCropper
shape="circle"
outputWidth={300}
outputHeight={300}
onCrop={async (base64) => {
const blob = await fetch(base64).then((r) => r.blob());
const formData = new FormData();
formData.append('avatar', blob, 'avatar.png');
await fetch('/api/upload', { method: 'POST', body: formData });
}}
>
<button>Upload Avatar</button>
</ImageCropper>
All Props
| Prop | Type | Default | Description |
|---|---|---|---|
onCrop |
(base64: string, cropArea: CropArea) => void |
required | Called when the user confirms the crop |
children |
ReactNode |
undefined |
Any clickable element that triggers the file picker. If omitted, a default "Choose Image" button is shown. |
shape |
"rect" | "circle" | "square" |
"rect" |
Shape of the crop mask |
outputWidth |
number |
undefined |
Output width in pixels. Locks aspect ratio when combined with outputHeight. |
outputHeight |
number |
undefined |
Output height in pixels |
aspectRatio |
number |
undefined |
Lock crop ratio without fixing output size (e.g. 16 / 9). Ignored when outputWidth and outputHeight are both set, or when shape is "square"/"circle". |
theme |
"dark" | "light" |
"dark" |
Visual theme of the modal |
showSizeBadge |
boolean |
true |
Show the "W × H px" badge under the crop area |
closeOnBackdropClick |
boolean |
false |
Whether clicking outside the modal closes it |
cropperWidth |
number |
520 |
Width of the crop canvas inside the modal |
cropperHeight |
number |
420 |
Height of the crop canvas inside the modal |
minCropWidth |
number |
40 |
Minimum crop box width in screen pixels |
minCropHeight |
number |
40 |
Minimum crop box height in screen pixels |
showGrid |
boolean |
true |
Show rule-of-thirds grid lines inside the crop area |
showZoom |
boolean |
true |
Show the zoom slider |
borderRadius |
string |
"0px" |
Border radius of the crop box. Only applies when shape is "rect". |
confirmLabel |
string |
"Crop & Apply" |
Label for the confirm button |
cancelLabel |
string |
"Cancel" |
Label for the cancel button |
accept |
string |
"image/*" |
Accepted file types for the file picker |
triggerClassName |
string |
"" |
CSS class applied to the trigger wrapper |
Ref Methods
interface ImageCropperRef {
open: () => void; // Programmatically open the file picker
}
CropArea Object
The second argument in onCrop contains the crop coordinates in natural image pixels (not screen pixels).
interface CropArea {
x: number; // left position in original image
y: number; // top position in original image
width: number; // width of cropped region
height: number; // height of cropped region
}
TypeScript Imports
import ImageCropper from '@ashutoshmishr0/image-cropper';
import type {
ImageCropperProps,
ImageCropperRef,
CropArea,
CropShape,
} from '@ashutoshmishr0/image-cropper';
Notes for Next.js
Add "use client" at the top of any file that uses this component — it relies on browser-only APIs (canvas, pointer events, FileReader) and cannot render on the server.
'use client';
import ImageCropper from '@ashutoshmishr0/image-cropper';
Common Mistakes
| Mistake | Fix |
|---|---|
Passing an image prop |
Not supported — the component manages file selection internally. Remove it and any manual <input type="file">. |
Using onCropComplete |
The callback prop is named onCrop, not onCropComplete. |
Using shape="rectangle" |
The valid values are "rect", "square", "circle" — not "rectangle". |
Setting aspectRatio on shape="circle" or "square" |
Unnecessary — ratio is auto-locked to 1:1 for these shapes. |