Forms

ImageCropper

A dialog for cropping a photo to a shape before it is used: drag the photo (or move it the way the arrow keys point) to choose what shows in the frame, zoom with the slider (or Plus and Minus), and Save hands the cropped image to onCrop as a File, ready for a form or an upload. The frame is a rectangle of aspect, or a circle for a profile photo. It always opens as a centered dialog, so dragging the photo never drags the dialog away.

Install

Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):

npx shadcn@latest init --base base --preset nova --yes --no-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13

Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):

npx shadcn@latest add ourostack/ouro-ui/image-cropper#v1.2.13

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/image-cropper

Its tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):

npx shadcn@latest add ourostack/ouro-ui/image-cropper#v1.2.13 ourostack/ouro-ui/image-cropper-tests#v1.2.13
import { ImageCropper } from "@/components/ouro/image-cropper/image-cropper";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<ImageCropper open={file !== null} onOpenChange={(open) => !open && setFile(null)} image={file} shape="circle" onCrop={upload} />

Use instead when

  • Use ImageField with crop instead to pick a photo and crop it in one field, and FileField for files that need no cropping.

Props

Type: ImageCropperProps.

aspect
Type: number
Default: 1
The crop's width divided by its height: 1 square, 16 / 9 wide.
image (required)
Type: File | Blob | string | null
The image to crop: a File from a picker, a Blob, or a URL the page may read pixels from (same origin or CORS).
labels
Type: ImageCropperLabels
The words it shows and says.
onCrop (required)
Type: (file: File) => void
Called with the cropped image, as a File, when the person saves. The dialog then closes.
onError
Type: ((reason: ImageCropperFailure) => void)
Called when the image cannot be opened or the crop cannot be saved. The cropper shows and announces labels.openError or labels.saveError itself; close it or offer another image.
onOpenChange (required)
Type: (open: boolean) => void
Called when it opens or closes: after Save, Cancel, Escape or the close button.
open (required)
Type: boolean
Whether the cropper's dialog is open.
outputWidth
Type: number
Default: 1024
The saved image's width in pixels; its height follows aspect.
shape
Type: "circle" | "rect"
Default: "rect"
circle shows a round frame for a profile photo; the saved image is still the square around it.
title
Type: string
Default: "Crop photo"
The dialog's title.
type
Type: "image/jpeg" | "image/png" | "image/webp"
Default: "image/jpeg"
The saved image's format.

Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

  • aspectRatio: Not a prop. The shape is aspect, width over height: aspect={16 / 9}.
  • onChange: Not a prop. The cropped image comes to onCrop: <ImageCropper onCrop={(file) => …} />.
  • round: Not a prop. Ouro UI uses shape: <ImageCropper shape="circle" />.
  • src: Not a prop. The image is image: <ImageCropper image={file} />.

Types

ImageCropperFailure

What went wrong: open, the browser cannot show the image (a format it cannot decode, or an address it may not read); save, it cannot encode the crop.

type ImageCropperFailure = "open" | "save";

ImageCropperLabels

The words the cropper shows and says, for apps in other languages.

interface ImageCropperLabels {
  /** @default "Save" */
  save?: string | undefined;
  /** @default "Cancel" */
  cancel?: string | undefined;
  /** The zoom slider's label. @default "Zoom" */
  zoom?: string | undefined;
  /** The crop area's name for screen readers. @default "Photo to crop" */
  area?: string | undefined;
  /** How to move the photo, read with the crop area. @default "Drag the photo, or move it with the arrow keys, to choose what shows. Plus and minus zoom." */
  instructions?: string | undefined;
  /** Shown and announced when the browser cannot open the image. @default "This photo could not be opened. Choose another one." */
  openError?: string | undefined;
  /** Shown and announced when the browser cannot save the crop; Save can be tried again. @default "The photo could not be saved. Try again." */
  saveError?: string | undefined;
}