Image Crop
A responsive, pointer- and keyboard-driven image cropping tool. Draw, drag, and 8-way resize a crop over any image or video, with fixed aspect ratios, circular crops, min/max bounds, rule-of-thirds guides, and percent/pixel crop callbacks. Self-contained Tailwind styling, zero dependencies.
A responsive image cropping tool. Wrap any <img> (or <video>) and the user
can draw a new crop, drag it around, and resize it from any of the eight
handles. A masked overlay dims everything outside the selection. The crop is
a controlled value: set it from onChange and pass it straight back in.
Every change reports the crop twice — once in pixels and once in percent.
Keep the percent crop in state so the selection stays correct when the
container resizes, and use the pixel crop to drive a canvas preview or an
upload. Pointer interaction uses pointer capture, and the selection and handles
are keyboard-accessible: focus the crop and nudge it with the arrow keys
(Shift for ×10, Ctrl/Cmd for ×100).
It is self-contained: zero dependencies, all styling is plain Tailwind, and the only dynamic values (the selection box and the SVG mask) are applied inline.
Installation
One-time setup: add the @ikui registry to your components.json.
{
"registries": {
"@ikui": "https://ik-ui.pages.dev/r/{name}.json"
}
}Then install the component:
pnpm dlx shadcn@latest add @ikui/image-cropUsage
import { ImageCrop, type Crop } from "@/components/image-crop";const [crop, setCrop] = useState<Crop>();
<ImageCrop crop={crop} onChange={(_, percentCrop) => setCrop(percentCrop)}>
<img src="/photo.jpg" alt="" />
</ImageCrop>;Examples
Fixed aspect ratio with preview
Pass aspect to lock the crop to a ratio — here 16 / 9. The helper functions
makeAspectCrop and centerCrop build a centered initial crop on image load,
and onComplete feeds the pixel crop into a <canvas> preview.
Circular crop
Set circularCrop to render the selection as a circle — pair it with aspect={1}
to keep it round (it warps to an oval otherwise). The mask is the only thing that
changes; the crop is still a rectangle, so clip the <canvas> to an ellipse to
get an avatar with transparent corners.
Custom styling
The selection and handles ship with a teal default, but
selectionClassName and handleClassName are merged onto them (later classes
win via cn), so you can restyle the crop region without forking the
component — here a dashed white border with small square handles.
Props
| Prop | Type | Default |
|---|---|---|
onChange | (crop: PixelCrop, percentCrop: PercentCrop) => void | - |
crop | Crop | - |
aspect | number | - |
circularCrop | boolean | false |
ruleOfThirds | boolean | false |
disabled | boolean | false |
locked | boolean | false |
keepSelection | boolean | false |
minWidth | number | - |
minHeight | number | - |
maxWidth | number | - |
maxHeight | number | - |
onComplete | (crop: PixelCrop, percentCrop: PercentCrop) => void | - |
onDragStart | (e: PointerEvent) => void | - |
onDragEnd | (e: PointerEvent) => void | - |
renderSelectionAddon | (state: ImageCropState) => ReactNode | - |
selectionClassName | string | - |
handleClassName | string | - |
