npm.io
1.0.2 • Published 3d ago

@ashutoshmishr0/image-cropper

Licence
MIT
Version
1.0.2
Deps
0
Size
62 kB
Vulns
0
Weekly
0

@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:

  1. Wrap any element (or use the built-in default button) as children
  2. User clicks it — a file picker opens automatically
  3. User selects an image — the crop modal opens
  4. User drags, zooms, and resizes the crop area
  5. User clicks "Crop & Apply"onCrop fires with the base64 PNG
  6. Modal closes — do whatever you want with the result (preview, upload, etc.)

Important: Do not pass your own <input type="file"> or an image prop. The component manages file selection internally — you only need onCrop.


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 shape is "square" or "circle", the crop ratio is automatically locked to 1:1 — you don't need to also pass aspectRatio.


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.

Keywords