Image Cropper
Preview: Default
Crop box over an image: drag to move, drag corners to resize, optional fixed aspect. Keyboard: arrow keys move the box, Alt+arrow keys resize it from the bottom-right corner (Shift = 10% steps for both). Values are fractions of the image (0–1) so they're resolution independent; `onChange` reports them.
Import
import { ImageCropper } from '@marbleui/ui'Props
<ImageCropper>
| Prop | Type | Default | Description |
|---|---|---|---|
| alt *required | string | — | |
| src *required | string | — | |
| aspect | number | — | |
| className | string | — | |
| onChange | (crop: CropRect) => void | — |
Usage notes
Crop box over an image, for avatars and cover photos. Pair it with cropToDataUrl to produce the cropped image.
const [crop, setCrop] = useState<CropRect | null>(null)
<ImageCropper src={photoUrl} alt="Profile photo to crop" aspect={1} onChange={setCrop} />
<Button isDisabled={!crop} onPress={async () => crop && setAvatar(await cropToDataUrl(photoUrl, crop, 256))}>Save photo</Button>
- The crop is reported as fractions of the image (
x,y,width,height, 0–1).onChangealso fires on mount and once the image loads. It’s uncontrolled: the starting box can’t be set from props. aspectis width ÷ height (1,16 / 9); leave it out for a free crop.- The crop box is a focusable slider (its value is the horizontal position;
aria-valuetextgives the whole box). Arrow keys move it and Alt+arrow keys resize it from the bottom-right corner (1%, Shift for 10%). cropToDataUrl(src, crop, outWidth = 512, crossOrigin = 'anonymous')returns a PNG data URL. Images from another origin load in CORS mode, so their host must sendAccess-Control-Allow-Origin; otherwise it rejects with an error that says so. Same-origin, blob and data URLs always work.
Example source · show· hide
import { useState } from 'react'
import { Button } from '../button'
import { demoImage } from '../lib/demo-images'
import { SegmentedControl, Segment } from '../segmented-control'
import { cropToDataUrl, ImageCropper, type CropRect } from './image-cropper'
const src = demoImage(2, 900, 600)
function Demo() {
const [aspect, setAspect] = useState<string>('free')
const [crop, setCrop] = useState<CropRect | null>(null)
const [out, setOut] = useState<string | null>(null)
return (
<div className="flex flex-col gap-3">
<SegmentedControl aria-label="Aspect ratio" value={aspect} onChange={(k) => setAspect(String(k))}><Segment id="free">Free</Segment><Segment id="1">1:1</Segment><Segment id="1.778">16:9</Segment></SegmentedControl>
<div className="flex items-start gap-4">
<ImageCropper src={src} alt="Landscape to crop" aspect={aspect === 'free' ? undefined : Number(aspect)} onChange={setCrop} className="w-80" />
<div className="flex flex-col gap-2"><Button size="sm" isDisabled={!crop} onPress={async () => crop && setOut(await cropToDataUrl(src, crop, 240))}>Crop</Button>{out && <img src={out} alt="Cropped result" className="w-32 rounded-md border border-border" />}</div>
</div>
</div>
)
}
export const styles = { Default: Demo }Source: packages/ui/src/image-cropper/image-cropper.tsx
Catalog
- Category
- media
- Priority
- Extra
- Primitive
- custom