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.tsxAbout
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.
Drag to crop
<ImageCropper
src={src}
aspect={1}
round={true}
onCrop={(blob) => upload(blob)}
/>Examples
Cropping an avatar
<ImageCropper src={src} aspect={1} round onCrop={(blob) => upload(blob)} />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'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultValue | string | number | readonly string[] | — | — |
| src | string | required | — |
| aspect | number | — | Width divided by height. Omit to crop freely. |
| value | CropRect | — | 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 | — | — |
| outputType | string | 'image/png' | Output type and quality for the exported blob. |
| outputQuality | number | 0.92 | — |
| maxOutput | number | — | Longest side of the export. The crop is scaled to fit. |
| round | boolean | false | Cosmetic mask for avatars. The export is still a rectangle. |
| cropLabel | string | 'Crop' | — |
| resetLabel | string | 'Reset' | — |
| actions | boolean | true | Hide the built-in buttons to drive it from your own footer. |
| alt | string | 'Image to crop' | — |
| origin | taints 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.
| Field | Type | Description |
|---|---|---|
| x | number | — |
| y | number | — |
| width | number | — |
| height | number | — |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| the gap it fills | dropzone → save | A 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. |
| handles | keyboard too | Arrow keys resize, shift for a larger step — a drag handle that only answers a pointer is unusable without one. |