Cropper
An image cropper built on react-easy-crop, as a compound the way a Base UI component is. Drag to reposition, pinch / scroll / slider to zoom; the image always covers the crop area. CropperSave renders the crop to a JPEG data URL via canvas (same-origin / CORS images only) and hands it to onSave.
A root given src is only the crop: the image is yours.
As a dialog
CropperDialog is the same root in a dialog from md up and a drawer below, for the two-step photo flow: a drop zone to pick or drop a photo, then the crop step with the zoom slider, Save and Choose another photo. The steps are CropperSteps, and every word is the parts' children, so the caller writes and translates the copy (the avatar flow does, in @repo/features/users/components/avatar-photo-dialog). Closing the dialog unmounts the parts, so the next open starts fresh.
Anatomy
<Cropper>
{" "}
{/* data-step="pick|crop" */}
<CropperStep value="pick">
<CropperPicker>
{" "}
{/* drop zone + trigger, one file */}
<IconCloudUpload />
<p>Drop your photo here</p>
<span className={buttonVariants()}>Select file</span>
</CropperPicker>
<CropperError match="rejected">Choose an image up to 10MB.</CropperError>
<CropperRemove onClick={remove}>Remove photo</CropperRemove>
</CropperStep>
<CropperStep value="crop">
<CropperStage /> {/* the crop canvas */}
<CropperZoom aria-label="Zoom" />
<CropperError>{error}</CropperError>
<CropperSave onSave={upload}>Save</CropperSave>
<CropperClear>Choose another photo</CropperClear>
</CropperStep>
</Cropper>Cropperis the root: the picked photo, the crop position and zoom, the step.data-steptells the stylesheet which one;disabled(while an upload runs) holds every action.CropperSteprenders its children only on its step, and is layout transparent.CropperPickeris a single-file file upload: the drop target and the trigger in one, withdata-state="dragging"while a drag hovers. A file outsideacceptor overmaxSizeis rejected, andCropperError match="rejected"shows.CropperStageandCropperZoomrender only once there is an image.CropperSaverunsonSavein a transition and wearsdata-pending.CropperCleardrops the picked photo and returns to the pick step;CropperRemoveis the destructive button for a photo you already have, and what it does is itsonClick.CropperErroris anAlert: withmatch="rejected"it shows on a rejected pick, otherwise whenever it has children.
Cropper props
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | — | The image to crop. Given, there is no pick step. |
aspect | number | 1 | Crop aspect ratio. |
shape | "round" | "rect" | "round" | Crop mask shape. |
outputSize | number | 512 | Width (px) of the output; height follows the aspect. |
disabled | boolean | false | Holds save, clear and remove. |
CropperDialog props
Cropper's props, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Whether the dialog is open. |
onOpenChange | (open: boolean) => void | — | Called when the dialog asks to open or close. |
title | ReactNode | — | The dialog's title. |
description | ReactNode | title | Read to screen readers; the drop hint does well. |
CropperPicker props
| Prop | Type | Default | Description |
|---|---|---|---|
accept | string | image/jpeg,image/png,image/webp,image/avif | What a canvas can decode. |
maxSize | number | 10 * 1024 * 1024 | In bytes. |