Image cropping is a common requirement in web applications - from profile picture uploads to photo editing tools. While CropperJS has been the go-to library for years, version 2.x brought a complete rewrite using modern web components. This created a gap: the existing react-cropper
library only supports v1.x.
In this tutorial, I'll show you how to implement image cropping in React using CropperJS 2.x with full TypeScript support.
Why CropperJS 2.x?
CropperJS 2.x is a ground-up rewrite that leverages modern web standards:
- ✅ Web Components - Built on native Custom Elements
- ✅ Better Performance - Optimized rendering and memory usage
- ✅ Modern Architecture - Cleaner API surface
- ✅ Framework Agnostic - Works anywhere web components work
However, using web components directly in React requires careful handling of refs, events, and lifecycle management.
Meet react-cropper-2
I built @imerljak/react-cropper-2
to provide idiomatic React bindings for CropperJS 2.x. The library offers three flexible APIs:
- Component API - Drop-in React component (easiest)
- Hook API - Composable hook with render function
- Advanced Hook API - Maximum control for power users
All with full TypeScript support.
Installation
npm install @imerljak/react-cropper-2 cropperjs@2
The library automatically imports and registers CropperJS web components, so you don't need to do it manually.
Quick Start: Component API
The simplest way to get started is using the <Cropper>
component:
import { useRef } from 'react';
import { Cropper, type CropperRef } from '@imerljak/react-cropper-2';
function ProfilePicture() {
const cropperRef = useRef<CropperRef>(null);
const handleSave = async () => {
const canvas = await cropperRef.current?.getCroppedCanvas();
if (canvas) {
const blob = await new Promise(resolve =>
canvas.toBlob(resolve, 'image/jpeg', 0.9)
);
// Upload blob to your server
console.log('Ready to upload:', blob);
}
};
return (
<div>
<Cropper
ref={cropperRef}
src="/profile-photo.jpg"
alt="Profile picture"
aspectRatio={1} // Square crop
onReady={() => console.log('Cropper ready!')}
onChange={(e) => console.log('Crop changed:', e.detail.bounds)}
/>
<button onClick={handleSave}>Save</button>
<button onClick={() => cropperRef.current?.reset()}>Reset</button>
</div>
);
}
Live Preview with useCropper Hook
For more control, use the useCropper
hook which provides built-in state management:
import { useState } from 'react';
import { useCropper } from '@imerljak/react-cropper-2';
function ImageEditor() {
const [previewUrl, setPreviewUrl] = useState<string>();
const { renderCropper, bounds, reset, getCroppedCanvas } = useCropper({
src: '/my-image.jpg',
alt: 'Edit me',
aspectRatio: 16 / 9,
onChange: async () => {
// Update preview on every crop change
const canvas = await getCroppedCanvas();
setPreviewUrl(canvas?.toDataURL('image/png'));
},
});
return (
<div className="editor-layout">
<div className="cropper-section">
{renderCropper({ style: { maxHeight: '500px' } })}
</div>
<div className="preview-section">
<h3>Preview</h3>
{previewUrl && <img src={previewUrl} alt="Preview" />}
{bounds && (
<pre>
{JSON.stringify(bounds, null, 2)}
</pre>
)}
<button onClick={reset}>Reset</button>
</div>
</div>
);
}
Advanced: Custom Cropper UI
Need complete control? Use useCropperAdvanced
to build custom UIs:
import { useCropperAdvanced } from '@imerljak/react-cropper-2';
function CustomCropper() {
const {
canvasRef,
selectionRef,
bounds,
setBounds,
reset,
} = useCropperAdvanced({
autoInitialize: true,
onReady: () => console.log('Ready!'),
});
const setSquareCrop = () => {
setBounds({ x: 50, y: 50, width: 300, height: 300 });
};
return (
<div>
<cropper-canvas ref={canvasRef} background>
<cropper-image
src="/image.jpg"
alt="Custom cropper"
rotatable
scalable
/>
<cropper-selection
ref={selectionRef}
initial-coverage={0.8}
movable
resizable
outlined
>
<cropper-grid role="grid" bordered covered />
<cropper-handle action="move" />
<cropper-handle action="n-resize" />
<cropper-handle action="e-resize" />
<cropper-handle action="s-resize" />
<cropper-handle action="w-resize" />
<cropper-handle action="ne-resize" />
<cropper-handle action="nw-resize" />
<cropper-handle action="se-resize" />
<cropper-handle action="sw-resize" />
</cropper-selection>
</cropper-canvas>
<div className="custom-controls">
<button onClick={setSquareCrop}>Square Crop</button>
<button onClick={reset}>Reset</button>
{bounds && (
<span>
{bounds.width}x{bounds.height}
</span>
)}
</div>
</div>
);
}
TypeScript Benefits
The library is TypeScript-first with comprehensive type definitions:
import type {
CropperRef,
CropperBounds,
CropperEventHandler,
GetCroppedCanvasOptions
} from '@imerljak/react-cropper-2';
// Fully typed ref
const cropperRef = useRef<CropperRef>(null);
// Type-safe event handlers
const handleChange: CropperEventHandler = (event) => {
const { bounds, canvas } = event.detail;
console.log(`Crop: ${bounds.width}x${bounds.height}`);
};
// Type-safe options
const options: GetCroppedCanvasOptions = {
width: 800,
height: 600,
fillColor: '#fff',
};
Common Use Cases
Avatar Upload with Circular Crop
function AvatarUpload() {
const cropperRef = useRef<CropperRef>(null);
const handleUpload = async () => {
const canvas = await cropperRef.current?.getCroppedCanvas({
width: 200,
height: 200,
});
if (canvas) {
const blob = await new Promise<Blob | null>(resolve =>
canvas.toBlob(resolve, 'image/jpeg', 0.95)
);
const formData = new FormData();
formData.append('avatar', blob!, 'avatar.jpg');
await fetch('/api/upload-avatar', {
method: 'POST',
body: formData,
});
}
};
return (
<Cropper
ref={cropperRef}
src="/uploaded-photo.jpg"
aspectRatio={1}
initialCoverage={0.8}
rounded // Makes it appear circular
/>
);
}
Multiple Aspect Ratios
function MultiAspectCropper() {
const [aspectRatio, setAspectRatio] = useState(16 / 9);
return (
<div>
<div className="aspect-ratio-controls">
<button onClick={() => setAspectRatio(16 / 9)}>16:9</button>
<button onClick={() => setAspectRatio(4 / 3)}>4:3</button>
<button onClick={() => setAspectRatio(1)}>1:1</button>
<button onClick={() => setAspectRatio(undefined)}>Free</button>
</div>
<Cropper
src="/image.jpg"
aspectRatio={aspectRatio}
/>
</div>
);
}
CORS Image Handling
When cropping images from different domains:
<Cropper
src="https://example.com/image.jpg"
crossOrigin="anonymous"
alt="Remote image"
/>
API Overview
Component Props
Prop | Type | Default | Description |
---|---|---|---|
src |
string |
required | Image URL |
aspectRatio |
number |
- | Aspect ratio (e.g., 16/9) |
initialCoverage |
number |
0.5 | Initial crop coverage (0-1) |
background |
boolean |
true | Show background |
movable |
boolean |
true | Allow moving selection |
resizable |
boolean |
true | Allow resizing |
zoomable |
boolean |
true | Allow zooming |
onReady |
function |
- | Fires when ready |
onChange |
function |
- | Fires on crop change |
Ref Methods
interface CropperRef {
getCanvas: () => CropperCanvasElement | null;
getBounds: () => CropperBounds | null;
setBounds: (bounds: Partial<CropperBounds>) => void;
reset: () => void;
clear: () => void;
getCroppedCanvas: (options?) => Promise<HTMLCanvasElement | null>;
}
Live Examples
Check out the interactive Storybook documentation with live examples covering:
- Basic cropping scenarios
- Different aspect ratios
- Custom styling
- Event handling patterns
- All three API approaches
- Advanced configurations
Browser Support
Works in all modern browsers that support:
- ES2020
- Web Components (Custom Elements)
- ES Modules
Comparison with react-cropper (v1)
If you're coming from react-cropper
(v1.x wrapper):
Feature | react-cropper | react-cropper-2 |
---|---|---|
CropperJS version | 1.x | 2.x |
Architecture | jQuery-based | Web Components |
TypeScript | Partial | Full, strict mode |
React 19 | ❌ | ✅ |
Bundle size | Larger | Smaller |
APIs | 1 (component) | 3 (component + hooks) |
When to Use Each API
- Component API - Quick integration, simple use cases, prefer components
- Hook API - Need state management, composable logic, custom layouts
- Advanced Hook API - Complete control, custom UI, complex interactions
Wrapping Up
react-cropper-2
brings CropperJS 2.x's modern web component architecture to React with three flexible APIs, full TypeScript support, and comprehensive testing.
Resources
Try it in your next React project and let me know what you think! Questions and feedback are always welcome.
Top comments (0)