react-image-crop-rotate
v0.1.0
Published
Headless React hooks and utilities for cropping and free-rotating images on a canvas. Bring your own UI.
Maintainers
Readme
react-image-crop-rotate
Headless React hooks and canvas utilities for cropping and free-rotating images before upload. Bring your own UI — no styling, no modal, no opinions about which cropper library you mount the state into.
- ✅ Headless: just hooks + pure canvas helpers
- ✅ Free rotation (any angle, not just 90°) + crop
- ✅ Output:
Blobready forFormDataupload - ✅ Works with
react-easy-crop, a custom canvas, or anything that emits a crop area - ✅ Tiny: zero runtime dependencies beyond React
- ✅ ESM + CJS, full TypeScript types
Install
npm install react-image-crop-rotate
# or
pnpm add react-image-crop-rotatePeer deps: react@>=18, react-dom@>=18.
Quick start (with react-easy-crop)
import { useImageCropRotate, fileToDataUrl } from "react-image-crop-rotate";
import Cropper from "react-easy-crop";
function MyUploader({
file,
onUploaded,
}: {
file: File;
onUploaded: (url: string) => void;
}) {
const [src, setSrc] = useState<string | null>(null);
const cropper = useImageCropRotate();
// Decode the file once
useEffect(() => {
fileToDataUrl(file).then(setSrc);
}, [file]);
if (!src) return null;
return (
<>
<div style={{ position: "relative", height: 400 }}>
<Cropper
image={src}
crop={cropper.crop}
zoom={cropper.zoom}
rotation={cropper.rotation}
aspect={1}
onCropChange={cropper.setCrop}
onZoomChange={cropper.setZoom}
onCropComplete={(_, area) => cropper.setCropArea(area)}
/>
</div>
<input
type="range"
min={-180}
max={180}
value={
cropper.rotation > 180 ? cropper.rotation - 360 : cropper.rotation
}
onChange={(e) => cropper.setRotation(Number(e.target.value))}
/>
<button
disabled={!cropper.cropArea}
onClick={async () => {
const blob = await cropper.getCroppedBlob(src);
const form = new FormData();
form.append("file", blob, "crop.png");
const res = await fetch("/upload", { method: "POST", body: form });
onUploaded((await res.json()).url);
}}
>
Upload
</button>
</>
);
}API
useImageCropRotate(options?)
Headless state container. Owns crop, zoom, rotation, and the latest cropArea reported by the UI; exposes getCroppedBlob / getRotatedBlob for upload.
const cropper = useImageCropRotate({
initialZoom: 1,
initialRotation: 0,
encode: { type: 'image/jpeg', quality: 0.9 },
});
cropper.crop; // { x, y }
cropper.zoom; // number
cropper.rotation; // degrees, [0, 360)
cropper.cropArea; // CropArea | null — set this from your cropper's onCropComplete
cropper.setCrop(point);
cropper.setZoom(z);
cropper.setRotation(deg);
cropper.setCropArea(area);
cropper.reset();
cropper.getCroppedBlob(source, overrides?); // → Promise<Blob>
cropper.getRotatedBlob(source, overrides?); // → Promise<Blob>cropImageToBlob(source, area, rotationDeg?, options?)
Pure function. Crops a source image (data URL, blob URL, http URL, or HTMLImageElement) into a Blob using the pixel area you supply. Pass the same rotationDeg you used while picking the area, or the crop will land in the wrong spot.
const blob = await cropImageToBlob(
dataUrl,
{ x: 10, y: 10, width: 200, height: 200 },
45,
);rotateImageToBlob(source, rotationDeg, options?)
Bake a rotation into the full image without cropping. Output canvas grows to fit so nothing is clipped.
const blob = await rotateImageToBlob(dataUrl, 90, {
type: "image/jpeg",
quality: 0.85,
});loadImage(src) / fileToDataUrl(file) / rotatedBoundingBox(w, h, rad) / normalizeAngle(deg)
Small utilities exported for completeness. See JSDoc for details.
License
MIT
