Move, zoom and turn a picture under a crop of any shape, then draw the crop into a file with cropImage.
$ pnpm dlx shadcn@latest add @dashral/image-cropperInstallation
$ pnpm dlx shadcn@latest add @dashral/image-cropperAdds the component, the components it builds on, its npm packages and the theme tokens it uses. Or by its full URL: https://dashral.com/r/image-cropper.json
When to use
- A picture to frame before it's saved: a cover or banner, a product photo, a crop of a set shape.
When not to use
- A profile photo or logo in a form: Avatar Upload, which brings the picker and the dialog.
- A picture only to look at, bigger: Lightbox.
Usage
onCropChange keeps the area; cropImage draws it into a file at the width you ask for.
"use client"
import { useState } from "react"
import { Button } from "@/components/dashral/button"
import { ImageCropper, cropImage, type CropArea } from "@/components/dashral/image-cropper"
export default function CoverImage() {
const [area, setArea] = useState<CropArea | null>(null)
const picture = "https://picsum.photos/id/1018/1600/1000"
async function save() {
if (!area) return
const blob = await cropImage(picture, area, { width: 1500, crossOrigin: "anonymous" })
console.log("upload", new File([blob], "cover.jpg", { type: blob.type }))
}
return (
<div className="grid max-w-xl gap-4">
<ImageCropper src={picture} aspect={3} crossOrigin="anonymous" onCropChange={setArea} aria-label="Cover image" />
<Button className="justify-self-end" onClick={save}>
Save cover
</Button>
</div>
)
}Using the npm package? Import from @dashral/ui/components/image-cropper instead, here and in the examples; the rest is the same.
Examples
API reference
ImageCropper
| Prop | Type | Default |
|---|---|---|
src | stringThe picture: a URL, or URL.createObjectURL(file). Required. | — |
aspect | numberWidth ÷ height of the crop: 1 for a square or circle, 3 for a wide cover. | 1 |
shape | "rect" | "circle"The crop's outline. A circle still crops a square. | "rect" |
zoom / defaultZoom | number1 fills the crop exactly. | 1 |
onZoomChange | (zoom: number) => void | — |
minZoom / maxZoom | number | 1 / 3 |
rotation / defaultRotation | numberDegrees, clockwise, a multiple of 90. | 0 |
onRotationChange | (rotation: number) => void | — |
onCropChange | (area: CropArea) => voidOn every change, and once the picture has loaded. | — |
showControls | booleanThe zoom slider and the rotate button under the picture. | true |
crossOrigin | "anonymous" | "use-credentials"For a picture on another site that allows it, so cropImage can read it. | — |
height | numberOf the picture area, in pixels; it's as wide as its container. | 288 |
disabled | boolean | false |
aria-label | stringThe picture area's name. | "Crop area" |
classNames | { viewport, image, frame, controls }Classes for each part. | — |
className | stringExtra classes, merged with cn() so yours win. | — |
cropImage()
cropImage(src, area, { width?, type?, quality?, crossOrigin? }) → Promise<Blob>: draws the crop, turned, at that width (the crop's own size by default). type is "image/jpeg" by default, with a white ground under transparency.
CropArea
In the picture's own pixels, before it's turned.
= { x: number; y: number; width: number; height: number; rotation: number }Keyboard
| Keys | Action |
|---|---|
| Tab | Focus the picture, then the zoom buttons, slider and rotate |
| ← → ↑ ↓ | Move the picture (Shift for bigger steps) |
| +or− | Zoom in or out |
Accessibility
- The picture area is a named group that can be focused, and says how to move and zoom it with the keyboard.
- Every control under it has a name: Zoom out, Zoom, Zoom in, Rotate 90°.
- A picture that won't load says so in an alert in its place, and the controls stay off.
Motion
- The picture fades in once it's loaded and placed. Zoom and turns glide (--duration-standard); dragging and pinching follow the finger exactly. Thirds show over the crop while it moves.
- With reduced motion, zoom and turns jump.
Related blocks
Whole sections built with Image Cropper or made for the same job, ready to drop in. Free blocks come with their code; Pro blocks come with Dashral Pro.

The workspace's name, logo, cover and colour, previewed live in the sidebar, the sign-in page, emails and invoices.
