Image Cropper

The step between picking an image and saving it. The crop is stored in natural image coordinates and exported with toBlob, so it survives a resize and uploads directly.

src/components/ui/image-cropper.tsx

About

From the component's own source, where it was written.

The step between picking an image and saving it.

A kit with a dropzone, a file input and an upload list still cannot set an avatar, because every avatar upload needs a square and every photo is a rectangle. Without this the crop happens on the server, or not at all and the image is squashed by CSS.

**The crop rectangle is stored in natural image coordinates**, not in displayed pixels. The display size depends on the container, which changes with the viewport; a crop expressed in screen pixels silently means something different after a resize, and produces a different output on a phone than on a desktop.

**The output is produced with canvas.toBlob, not toDataURL.** A data URL is base64, which is a third larger, has to be built as one string in memory, and then usually gets converted back to a blob to upload anyway. toBlob hands you something you can put straight in a FormData.

**A cross-origin image taints the canvas** and makes toBlob throw a SecurityError. crossOrigin="anonymous" is set on the image so a server that sends permissive CORS headers works; one that does not will fail at export, and onError reports it rather than leaving a dead button.

Install

npx astralyx-ui add image-cropper

Usage

import { ImageCropper } from '@/components/ui/image-cropper'

<ImageCropper src={src} aspect={1} round onCrop={(blob) => upload(blob)} />

Composer

Change the props and the source below follows.

Image to crop

Drag to crop

Props
tsx
<ImageCropper
  src={src}
  aspect={1}
  round={true}
  onCrop={(blob) => upload(blob)}
/>

Examples

Cropping an avatar

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

ImageCropper

Extends Omit<ComponentProps<'div'>, 'onChange' | 'onError'>.

PropTypeDefaultDescription
defaultValuestring | number | readonly string[]——
srcstringrequired—
aspectnumber—Width divided by height. Omit to crop freely.
valueCropRect—Natural image pixels, not displayed ones. A crop in screen pixels silently means something different after a resize, and produces a different output on a phone than on a desktop.
onChange(crop: CropRect) => void—Natural image pixels, not displayed ones. A crop in screen pixels silently means something different after a resize, and produces a different output on a phone than on a desktop.
onCrop(blob: Blob, crop: CropRect) => void—toBlob, not toDataURL: base64 is a third larger, has to be built as one string, and usually gets converted back to a blob to upload anyway.
onError(error: Error) => void——
outputTypestring'image/png'Output type and quality for the exported blob.
outputQualitynumber0.92—
maxOutputnumber—Longest side of the export. The crop is scaled to fit.
roundbooleanfalseCosmetic mask for avatars. The export is still a rectangle.
cropLabelstring'Crop'—
resetLabelstring'Reset'—
actionsbooleantrueHide the built-in buttons to drive it from your own footer.
altstring'Image to crop'—
origintaints the canvas—crossOrigin="anonymous" is set so a CORS-permissive server works; one that is not will throw at export, and onError reports it rather than leaving a dead button.

Types

CropRect

The step between picking an image and saving it.

FieldTypeDescription
xnumber—
ynumber—
widthnumber—
heightnumber—

Behaviour

TopicDetailDescription
the gap it fillsdropzone → saveA kit with a dropzone, a file input and an upload list still cannot set an avatar, because every avatar needs a square and every photo is a rectangle.
handleskeyboard tooArrow keys resize, shift for a larger step — a drag handle that only answers a pointer is unusable without one.