Image Cropper
ProCrop, with the parts that silently corrupt the result handled: EXIF orientation, the browser's canvas ceiling, and a quality setting PNG ignores. An image cropper fails by producing a picture rather than an error, so it reports what the crop will actually produce rather than what was selected.
Complexity tier: Interactive The primitive bar, plus declared async states, a keyboard model, focus restoration and undo for destructive actions.
import { SvImageCropperComponent } from '@styloviz-pro/image-editor';Demo & usage
Basic usage
ProUsage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
A square avatar crop
ProWhat the crop will actually produce
ProThe plan is exposed rather than applied silently: a crop past what a browser will render is downscaled, and past that ceiling a canvas is not an error — it is blank, and toBlob succeeds. The warnings are the only thing standing between the user and a transparent file.
A photograph that is not stored the way it is shown
ProA phone photograph is stored sideways with a tag saying which way is up. The component cannot read that tag — it lives in the file's bytes and an <img> does not expose it — so a host that decoded the file passes it in. Leave it at 1 for an image the browser has already auto-oriented.
The geometry on its own
ProExported so a host with its own surface — an inline crop inside an uploader, a canvas editor — gets the same arithmetic rather than a second copy that drifts.
When to use
- Letting a user choose the part of an image you will store — an avatar, a cover, a thumbnail.
- You need the crop in the image's own pixels, not the ones on screen.
- For a before/after wipe rather than a selection, use Image Compare.
Async states
None — this component has nothing to load, so it has no loading, empty or error state to drive.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| src | string | '' | Image URL. Anything an `<img>` accepts, including a blob: or data: URL. |
| alt | string | '' | Alt text for the image being cropped. |
| aspect | number | 0 | width ÷ height. 0 leaves the crop free. |
| minSize | number | 24 | Smallest crop, in displayed pixels. |
| orientation | SvExifOrientation | 1 | The image's EXIF orientation, when the host knows it. THE COMPONENT CANNOT READ THIS ITSELF, AND PRETENDING OTHERWISE WOULD BE THE LIE. The tag lives in the file's bytes; an `<img>` element does not expose it. A host that decoded the file already has it, and one that did not should leave this at 1 — which is correct for every image a browser has already auto-oriented, and that is most of them today. |
| outputType | SvImageType | 'image/png' | Format of the exported image: `'image/png'`, `'image/jpeg'` or `'image/webp'`. |
| quality | number | 0.92 | Encoder quality from 0 to 1 for JPEG and WebP exports; PNG ignores it. |
| flattenTo | string | '#ffffff' | The colour transparency is flattened onto for a format with no alpha. Only JPEG needs it, and only JPEG gets it. A canvas starts transparent and JPEG has no alpha channel, so without a fill a logo on a transparent ground exports as artwork on BLACK — a valid file, no error, and the classic silent corruption. White is the default because it is what printing does and what a user expects. |
| maxDimension | number | undefined | undefined | Longest side the export may have, in px; a larger crop is scaled down. Unset uses the browser canvas limit (16,384). |
| maxPixels | number | undefined | undefined | Total pixel budget for the export; a larger crop is scaled down to fit. Unset uses the browser canvas limit (16,777,216). |
| disabled | boolean | false | Locks the crop box and its resize handles against pointer and keyboard edits. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| cropChange | SvCropRect | The crop, in SOURCE pixels, whenever it changes. |
| cropped | Blob | The rendered image, once `crop()` resolves. |
| cropFailed | { reason: string; error?: unknown } | An export that could not be produced, with the reason. |
Customization & theming
- Lock the shape with `aspect`, bound it with `minSize`, and pick the encoding with `outputType` and `quality`; `maxDimension` and `maxPixels` override the conservative canvas ceilings.
- `flattenTo` (default `'#ffffff'`) is the colour a JPEG export is filled with before the draw — JPEG has no alpha, so a transparent ground would otherwise come out black. PNG and WebP keep their transparency and are never filled.
- Read the selection with `cropChange` (source pixels) or `sourceCrop()`, the rendering with `plan()` and `outputLabel()`, and produce the image with `cropImage()`.
- `svDefaultCrop`, `svResizeCrop`, `svMoveCrop`, `svClampCrop`, `svCropToSource`, `svOrientedSize`, `svOrientationTransform` and `svOutputPlan` are exported for a host building its own surface.
Best practices
- Do: Show `plan().warnings`. A crop the browser cannot render is downscaled, and past that ceiling the canvas is blank rather than an error — `toBlob` still calls back with a valid, fully transparent file.
- Do: Pass `orientation` when you decoded the file yourself and know the EXIF tag. A portrait phone photo is stored landscape, and cropping the stored orientation lands on a region that is not in the picture.
- Do: Use JPEG or WebP when you want compression. `toBlob(cb, 'image/png', 0.6)` ignores the 0.6 silently, so a quality slider wired to a PNG export does nothing at all.
- Don't: Don't send `cropChange` straight to a canvas without checking it is the SOURCE rect. The crop is dragged in display pixels; the emitted rect is already converted, and the two differ by whatever the browser laid the image out at.
Accessibility
- The crop box is a focusable `role="group"` whose label reports what the crop will produce, and every one of the eight handles is a labelled button in the tab order — "drag a box" has no non-pointer equivalent unless one is built, so arrows nudge by a pixel, Shift by ten, and Escape resets.
- Only the keys it handles are swallowed: Tab and everything else pass through, so the crop box cannot become a focus trap.
- The dimming mask is `pointer-events-none` and `aria-hidden`, so it neither swallows a drag aimed at a handle nor appears in the accessibility tree.
- What the crop will produce — including any downscale and its reason — is a `role="status"` caption rather than something the user has to infer from the file they get later.
Feedback on Image Cropper
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.