Cropper

Inspect

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.

Inspect

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.

Inspect

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>
  • Cropper is the root: the picked photo, the crop position and zoom, the step. data-step tells the stylesheet which one; disabled (while an upload runs) holds every action.
  • CropperStep renders its children only on its step, and is layout transparent.
  • CropperPicker is a single-file file upload: the drop target and the trigger in one, with data-state="dragging" while a drag hovers. A file outside accept or over maxSize is rejected, and CropperError match="rejected" shows.
  • CropperStage and CropperZoom render only once there is an image.
  • CropperSave runs onSave in a transition and wears data-pending.
  • CropperClear drops the picked photo and returns to the pick step; CropperRemove is the destructive button for a photo you already have, and what it does is its onClick.
  • CropperError is an Alert: with match="rejected" it shows on a rejected pick, otherwise whenever it has children.

Cropper props

PropTypeDefaultDescription
srcstring—The image to crop. Given, there is no pick step.
aspectnumber1Crop aspect ratio.
shape"round" | "rect""round"Crop mask shape.
outputSizenumber512Width (px) of the output; height follows the aspect.
disabledbooleanfalseHolds save, clear and remove.

CropperDialog props

Cropper's props, plus:

PropTypeDefaultDescription
openboolean—Whether the dialog is open.
onOpenChange(open: boolean) => void—Called when the dialog asks to open or close.
titleReactNode—The dialog's title.
descriptionReactNodetitleRead to screen readers; the drop hint does well.

CropperPicker props

PropTypeDefaultDescription
acceptstringimage/jpeg,image/png,image/webp,image/avifWhat a canvas can decode.
maxSizenumber10 * 1024 * 1024In bytes.