A React component to crop images/videos with easy interactions
npx @tessl/cli install tessl/npm-react-easy-crop@5.5.0React Easy Crop is a React component library that provides an intuitive interface for cropping images and videos with drag, zoom, and rotate interactions. It supports multiple media formats, offers mobile-friendly touch gestures, and provides crop coordinates in both pixels and percentages.
npm install react-easy-cropimport Cropper from "react-easy-crop";
import type { CropperProps, Area, Point, Size, MediaSize, VideoSrc } from "react-easy-crop";For helper functions:
import {
getInitialCropFromCroppedAreaPixels,
getInitialCropFromCroppedAreaPercentages
} from "react-easy-crop";CommonJS:
const Cropper = require("react-easy-crop").default;
const { getInitialCropFromCroppedAreaPixels, getInitialCropFromCroppedAreaPercentages } = require("react-easy-crop");import React, { useState, useCallback } from 'react';
import Cropper from 'react-easy-crop';
import type { Area, Point } from 'react-easy-crop';
const CropDemo = () => {
const [crop, setCrop] = useState<Point>({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [rotation, setRotation] = useState(0);
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
const onCropComplete = useCallback(
(croppedArea: Area, croppedAreaPixels: Area) => {
setCroppedAreaPixels(croppedAreaPixels);
},
[]
);
return (
<div style={{ position: 'relative', width: '100%', height: 400 }}>
<Cropper
image="/path/to/image.jpg"
crop={crop}
zoom={zoom}
rotation={rotation}
aspect={4 / 3}
onCropChange={setCrop}
onZoomChange={setZoom}
onRotationChange={setRotation}
onCropComplete={onCropComplete}
style={{}}
classes={{}}
mediaProps={{}}
cropperProps={{}}
/>
</div>
);
};The main React component for image and video cropping with interactive controls.
interface CropperProps {
/** Current crop position */
crop: Point;
/** Current zoom level */
zoom: number;
/** Current rotation in degrees */
rotation: number;
/** Crop aspect ratio */
aspect: number;
/** Crop position change callback */
onCropChange: (location: Point) => void;
/** Image URL or base64 string */
image?: string;
/** Video source(s) */
video?: string | VideoSrc[];
/** Custom CSS transform */
transform?: string;
/** Minimum zoom level */
minZoom: number;
/** Maximum zoom level */
maxZoom: number;
/** Shape of crop area */
cropShape: 'rect' | 'round';
/** Fixed crop size */
cropSize?: Size;
/** Media object fit (default: 'contain') */
objectFit?: 'contain' | 'cover' | 'horizontal-cover' | 'vertical-cover';
/** Show crop grid overlay */
showGrid?: boolean;
/** Zoom sensitivity */
zoomSpeed: number;
/** Enable zoom with mouse wheel */
zoomWithScroll?: boolean;
/** Round crop area pixel values */
roundCropAreaPixels?: boolean;
/** Restrict crop position within media bounds */
restrictPosition: boolean;
/** Keyboard navigation step size */
keyboardStep: number;
/** Zoom level change callback */
onZoomChange?: (zoom: number) => void;
/** Rotation change callback */
onRotationChange?: (rotation: number) => void;
/** Crop interaction complete callback */
onCropComplete?: (croppedArea: Area, croppedAreaPixels: Area) => void;
/** Crop area change callback */
onCropAreaChange?: (croppedArea: Area, croppedAreaPixels: Area) => void;
/** Crop size change callback */
onCropSizeChange?: (cropSize: Size) => void;
/** Interaction start callback */
onInteractionStart?: () => void;
/** Interaction end callback */
onInteractionEnd?: () => void;
/** Media loaded callback */
onMediaLoaded?: (mediaSize: MediaSize) => void;
/** Custom styles */
style: {
containerStyle?: React.CSSProperties;
mediaStyle?: React.CSSProperties;
cropAreaStyle?: React.CSSProperties;
};
/** Custom CSS classes */
classes: {
containerClassName?: string;
mediaClassName?: string;
cropAreaClassName?: string;
};
/** Props passed to media element */
mediaProps: React.ImgHTMLAttributes<HTMLElement> | React.VideoHTMLAttributes<HTMLElement>;
/** Props passed to cropper div */
cropperProps: React.HTMLAttributes<HTMLDivElement>;
/** Disable automatic CSS injection */
disableAutomaticStylesInjection?: boolean;
/** Initial crop area in pixels */
initialCroppedAreaPixels?: Area;
/** Initial crop area in percentages */
initialCroppedAreaPercentages?: Area;
/** Touch request handler */
onTouchRequest?: (e: React.TouchEvent<HTMLDivElement>) => boolean;
/** Wheel request handler */
onWheelRequest?: (e: WheelEvent) => boolean;
/** Cropper ref callback */
setCropperRef?: (ref: React.RefObject<HTMLDivElement>) => void;
/** Image ref callback */
setImageRef?: (ref: React.RefObject<HTMLImageElement>) => void;
/** Video ref callback */
setVideoRef?: (ref: React.RefObject<HTMLVideoElement>) => void;
/** Media size callback */
setMediaSize?: (size: MediaSize) => void;
/** Crop size callback */
setCropSize?: (size: Size) => void;
/** CSP nonce for injected styles */
nonce?: string;
}
declare const Cropper: React.ComponentType<CropperProps>;
export default Cropper;Functions to compute initial crop settings from predefined cropped areas.
/**
* Compute initial crop position and zoom from pixel coordinates
* @param croppedAreaPixels - The crop area in pixels
* @param mediaSize - The media dimensions
* @param rotation - Current rotation in degrees (default: 0)
* @param cropSize - The crop area size
* @param minZoom - Minimum zoom level
* @param maxZoom - Maximum zoom level
* @returns Initial crop position and zoom level
*/
function getInitialCropFromCroppedAreaPixels(
croppedAreaPixels: Area,
mediaSize: MediaSize,
rotation?: number,
cropSize: Size,
minZoom: number,
maxZoom: number
): { crop: Point; zoom: number };
/**
* Compute initial crop position and zoom from percentage coordinates
* @param croppedAreaPercentages - The crop area in percentages
* @param mediaSize - The media dimensions
* @param rotation - Current rotation in degrees (required parameter)
* @param cropSize - The crop area size
* @param minZoom - Minimum zoom level
* @param maxZoom - Maximum zoom level
* @returns Initial crop position and zoom level
*/
function getInitialCropFromCroppedAreaPercentages(
croppedAreaPercentages: Area,
mediaSize: MediaSize,
rotation: number,
cropSize: Size,
minZoom: number,
maxZoom: number
): { crop: Point; zoom: number };/** Size dimensions */
interface Size {
width: number;
height: number;
}
/** Media size with natural dimensions */
interface MediaSize {
width: number;
height: number;
naturalWidth: number;
naturalHeight: number;
}
/** Point coordinates */
interface Point {
x: number;
y: number;
}
/** Rectangular area definition */
interface Area {
width: number;
height: number;
x: number;
y: number;
}
/** Video source configuration */
interface VideoSrc {
src: string;
type?: string;
}import React, { useState } from 'react';
import Cropper from 'react-easy-crop';
import type { Area, Point } from 'react-easy-crop';
const ImageCropper = ({ imageSrc }: { imageSrc: string }) => {
const [crop, setCrop] = useState<Point>({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [croppedArea, setCroppedArea] = useState<Area | null>(null);
return (
<div style={{ position: 'relative', width: '100%', height: 400 }}>
<Cropper
image={imageSrc}
crop={crop}
zoom={zoom}
aspect={16 / 9}
onCropChange={setCrop}
onZoomChange={setZoom}
onCropComplete={(_, croppedAreaPixels) => setCroppedArea(croppedAreaPixels)}
showGrid={true}
cropShape="rect"
style={{}}
classes={{}}
mediaProps={{}}
cropperProps={{}}
/>
</div>
);
};import React, { useState } from 'react';
import Cropper from 'react-easy-crop';
import type { VideoSrc, Point } from 'react-easy-crop';
const VideoCropper = () => {
const [crop, setCrop] = useState<Point>({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const videoSources: VideoSrc[] = [
{ src: '/video.mp4', type: 'video/mp4' },
{ src: '/video.webm', type: 'video/webm' }
];
return (
<div style={{ position: 'relative', width: '100%', height: 400 }}>
<Cropper
video={videoSources}
crop={crop}
zoom={zoom}
aspect={1}
onCropChange={setCrop}
onZoomChange={setZoom}
cropShape="round"
showGrid={false}
style={{}}
classes={{}}
mediaProps={{}}
cropperProps={{}}
/>
</div>
);
};import React, { useState } from 'react';
import Cropper from 'react-easy-crop';
import type { Point } from 'react-easy-crop';
const CircularCropper = ({ imageSrc }: { imageSrc: string }) => {
const [crop, setCrop] = useState<Point>({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
return (
<div style={{ position: 'relative', width: '100%', height: 400 }}>
<Cropper
image={imageSrc}
crop={crop}
zoom={zoom}
aspect={1}
cropShape="round"
onCropChange={setCrop}
onZoomChange={setZoom}
showGrid={false}
style={{
containerStyle: {
backgroundColor: '#333'
},
cropAreaStyle: {
border: '2px solid #fff'
}
}}
classes={{
containerClassName: 'custom-cropper-container'
}}
mediaProps={{}}
cropperProps={{}}
/>
</div>
);
};import React, { useState, useEffect } from 'react';
import Cropper, { getInitialCropFromCroppedAreaPixels } from 'react-easy-crop';
import type { Area, Point, MediaSize } from 'react-easy-crop';
const CropperWithInitialCrop = ({ imageSrc, savedCropPixels }: {
imageSrc: string;
savedCropPixels: Area;
}) => {
const [crop, setCrop] = useState<Point>({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [mediaSize, setMediaSize] = useState<MediaSize | null>(null);
useEffect(() => {
if (mediaSize && savedCropPixels) {
const { crop: initialCrop, zoom: initialZoom } = getInitialCropFromCroppedAreaPixels(
savedCropPixels,
mediaSize,
0, // rotation
{ width: 300, height: 200 }, // cropSize
1, // minZoom
3 // maxZoom
);
setCrop(initialCrop);
setZoom(initialZoom);
}
}, [mediaSize, savedCropPixels]);
return (
<div style={{ position: 'relative', width: '100%', height: 400 }}>
<Cropper
image={imageSrc}
crop={crop}
zoom={zoom}
aspect={3 / 2}
onCropChange={setCrop}
onZoomChange={setZoom}
onMediaLoaded={setMediaSize}
cropSize={{ width: 300, height: 200 }}
style={{}}
classes={{}}
mediaProps={{}}
cropperProps={{}}
/>
</div>
);
};