Image Cropper
Headless image cropper: aspect-locked crop box, zoom, rotation, keyboard nudging, canvas export.
|
|
| Package |
@vizejs/ui/image-cropper |
| Maturity |
stable |
| Own the source |
vize lib pull image-cropper |
| Requires |
context, controllable-state, id, measure |
| Aliases |
image cropper, crop, avatar cropper, photo editor, image crop |
| Covers |
Cropper.js, react-easy-crop, react-image-crop, Ark UI Image Cropper |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { ImageCropperArea, ImageCropperGrid, ImageCropperHandle, ImageCropperImage, ImageCropper, ImageCropperRoot, ImageCropperViewport } from "@vizejs/ui/image-cropper";
Or copy the source into your project with vize lib pull image-cropper (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Square avatar cropper with a rule-of-thirds grid, corner handles, and controlled zoom and rotation. -->
<script setup lang="ts">
import { ref, useId } from "vue";
import {
ImageCropper,
ImageCropperArea,
ImageCropperGrid,
ImageCropperHandle,
ImageCropperImage,
ImageCropperViewport,
} from "@vizejs/ui/image-cropper";
const zoom = ref(1);
const rotation = ref(0);
const zoomId = useId();
// The viewport needs a size to fit the image into.
const viewportSize = { inlineSize: "320px", blockSize: "240px" };
function rotate(): void {
rotation.value = (rotation.value + 90) % 360;
}
</script>
<template>
<ImageCropper
v-slot="{ crop }"
v-model:zoom="zoom"
v-model:rotation="rotation"
:aspect-ratio="1"
:max-zoom="3"
>
<ImageCropperViewport :style="viewportSize">
<ImageCropperImage src="/media/profile-photo.jpg" alt="Profile photo to crop" />
<ImageCropperArea>
<ImageCropperGrid />
<ImageCropperHandle position="nw" />
<ImageCropperHandle position="ne" />
<ImageCropperHandle position="sw" />
<ImageCropperHandle position="se" />
</ImageCropperArea>
</ImageCropperViewport>
<label :for="zoomId">Zoom</label>
<input :id="zoomId" v-model.number="zoom" type="range" min="1" max="3" step="0.1" />
<button type="button" @click="rotate">Rotate 90°</button>
<output>{{
crop ? `${Math.round(crop.width)} × ${Math.round(crop.height)} px` : "Loading image"
}}</output>
</ImageCropper>
</template>
API
ImageCropperArea
Source: src/families/media/image-cropper/image-cropper-area.vue
Slots
| Slot |
Slot props |
Description |
default |
ImageCropperSlotState |
Handles, grid, and other overlays inside the crop box. Receives the cropper state. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
ImageCropperGrid
Source: src/families/media/image-cropper/image-cropper-grid.vue
Props
| Prop |
Type |
Default |
Description |
divisions |
number |
3 |
Number of equal columns and rows; 3 draws the rule of thirds. |
Slots
| Slot |
Slot props |
Description |
default |
ImageCropperSlotState |
Optional extra overlay content. Receives the cropper state. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
ImageCropperHandle
Source: src/families/media/image-cropper/image-cropper-handle.vue
Props
| Prop |
Type |
Default |
Description |
position (required) |
ImageCropperHandlePosition |
required |
Edge or corner this handle resizes, by compass direction. |
Slots
| Slot |
Slot props |
Description |
default |
ImageCropperSlotState |
Optional handle content. Receives the cropper state. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
ImageCropperImage
Source: src/families/media/image-cropper/image-cropper-image.vue
Props
| Prop |
Type |
Default |
Description |
src (required) |
string |
required |
Image source. Relative, https:, blob:, and base64 data:image/* URLs are accepted; unsafe schemes are dropped. Use CORS-enabled URLs to export crops. |
allowInsecure |
boolean |
false |
Permit unencrypted http: sources for local development. |
alt (required) |
string |
required |
Native alternative text. |
crossOrigin |
"" | "anonymous" | "use-credentials" |
undefined |
Native CORS mode; anonymous keeps canvas exports untainted for CORS images. |
referrerPolicy |
ReferrerPolicy |
undefined |
Native referrer policy. |
Events
| Event |
Payload |
Description |
load |
[nativeEvent: Event | null] |
Fired after the image loads and its natural size is known. |
error |
[nativeEvent: Event] |
Fired when the image fails to load. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
ImageCropper
Source: src/families/media/image-cropper/image-cropper-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
modelValue |
CropArea |
undefined |
Controlled crop in rotated-image pixels (v-model). undefined selects uncontrolled behavior. |
defaultValue |
CropArea |
undefined |
Initial crop for uncontrolled use. undefined centers a crop covering autoCropArea. |
zoom |
number |
undefined |
Controlled zoom factor (v-model:zoom); 1 fits the whole image. |
defaultZoom |
number |
1 |
Initial zoom for uncontrolled use. |
rotation |
number |
undefined |
Controlled rotation in degrees (v-model:rotation). |
defaultRotation |
number |
0 |
Initial rotation for uncontrolled use. |
aspectRatio |
number |
undefined |
Locked width / height ratio. undefined allows free resizing. |
minWidth |
number |
undefined |
Minimum crop width in image pixels. |
minHeight |
number |
undefined |
Minimum crop height in image pixels. |
maxWidth |
number |
undefined |
Maximum crop width in image pixels. |
maxHeight |
number |
undefined |
Maximum crop height in image pixels. |
minZoom |
number |
1 |
Smallest zoom factor. |
maxZoom |
number |
5 |
Largest zoom factor. |
zoomStep |
number |
0.1 |
Relative zoom change per key press or wheel notch. |
rotationStep |
number |
90 |
Degrees rotated by [ and ]. |
nudgeStep |
number |
1 |
Image pixels moved per arrow key press (Shift multiplies by 10). |
autoCropArea |
number |
0.8 |
Fraction of the image the initial centered crop covers. |
disabled |
boolean |
false |
Suppress every pointer and keyboard interaction. |
messages |
Partial<ImageCropperMessages> |
undefined |
Localized accessible text. Missing keys fall back to English. |
Events
| Event |
Payload |
Description |
update:modelValue |
[crop: CropArea] |
Fired when the crop requests a new controlled value. |
update:zoom |
[zoom: number] |
Fired when zoom requests a new controlled value. |
update:rotation |
[rotation: number] |
Fired when rotation requests a new controlled value. |
change |
[crop: CropArea, reason: ImageCropperChangeReason] |
Fired after every distinct crop request, with its cause. |
cropEnd |
[crop: CropArea] |
Fired when a pointer interaction or key press finishes changing the crop. |
Slots
| Slot |
Slot props |
Description |
default |
ImageCropperSlotState |
Viewport, image, area, handles, and controls. Receives the cropper state. |
Exposed
| Member |
Type |
Description |
crop |
— |
|
disabled |
— |
|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
interaction |
— |
|
naturalSize |
— |
|
ready |
— |
|
reset |
— |
|
rotation |
— |
|
setCrop |
— |
|
setRotation |
— |
|
setZoom |
— |
|
toBlob |
— |
|
toDataUrl |
— |
|
zoom |
— |
|
ImageCropperRoot
Source: src/families/media/image-cropper/image-cropper-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
modelValue |
CropArea |
undefined |
Controlled crop in rotated-image pixels (v-model). undefined selects uncontrolled behavior. |
defaultValue |
CropArea |
undefined |
Initial crop for uncontrolled use. undefined centers a crop covering autoCropArea. |
zoom |
number |
undefined |
Controlled zoom factor (v-model:zoom); 1 fits the whole image. |
defaultZoom |
number |
1 |
Initial zoom for uncontrolled use. |
rotation |
number |
undefined |
Controlled rotation in degrees (v-model:rotation). |
defaultRotation |
number |
0 |
Initial rotation for uncontrolled use. |
aspectRatio |
number |
undefined |
Locked width / height ratio. undefined allows free resizing. |
minWidth |
number |
undefined |
Minimum crop width in image pixels. |
minHeight |
number |
undefined |
Minimum crop height in image pixels. |
maxWidth |
number |
undefined |
Maximum crop width in image pixels. |
maxHeight |
number |
undefined |
Maximum crop height in image pixels. |
minZoom |
number |
1 |
Smallest zoom factor. |
maxZoom |
number |
5 |
Largest zoom factor. |
zoomStep |
number |
0.1 |
Relative zoom change per key press or wheel notch. |
rotationStep |
number |
90 |
Degrees rotated by [ and ]. |
nudgeStep |
number |
1 |
Image pixels moved per arrow key press (Shift multiplies by 10). |
autoCropArea |
number |
0.8 |
Fraction of the image the initial centered crop covers. |
disabled |
boolean |
false |
Suppress every pointer and keyboard interaction. |
messages |
Partial<ImageCropperMessages> |
undefined |
Localized accessible text. Missing keys fall back to English. |
Events
| Event |
Payload |
Description |
update:modelValue |
[crop: CropArea] |
Fired when the crop requests a new controlled value. |
update:zoom |
[zoom: number] |
Fired when zoom requests a new controlled value. |
update:rotation |
[rotation: number] |
Fired when rotation requests a new controlled value. |
change |
[crop: CropArea, reason: ImageCropperChangeReason] |
Fired after every distinct crop request, with its cause. |
cropEnd |
[crop: CropArea] |
Fired when a pointer interaction or key press finishes changing the crop. |
Slots
| Slot |
Slot props |
Description |
default |
ImageCropperSlotState |
Viewport, image, area, handles, and controls. Receives the cropper state. |
Exposed
| Member |
Type |
Description |
crop |
CropArea | null |
Current crop, or null until the image size is known. |
zoom |
number |
Current zoom factor (1 fits the whole image). |
rotation |
number |
Current rotation in degrees, normalized to [0, 360). |
naturalSize |
CropSize | null |
Natural image size, or null until the image loads. |
ready |
boolean |
Whether image and viewport are measured and geometry is published. |
interaction |
ImageCropperInteraction |
Pointer or keyboard interaction in progress. |
disabled |
boolean |
Whether every interaction is suppressed. |
element |
HTMLDivElement | null |
Rendered root element. |
setCrop |
(crop: CropArea) => boolean |
Request a crop (clamped to the constraints). Reports whether it changed. |
setZoom |
(zoom: number) => boolean |
Request a zoom factor (clamped). Reports whether it changed. |
setRotation |
(degrees: number) => boolean |
Request a rotation in degrees. Reports whether it changed. |
reset |
() => void |
Restore the centered default crop, zoom 1, and rotation 0. |
toBlob |
(options?: Omit<CropImageOptions, "rotation">) => Promise<Blob> |
Crop the loaded image into a Blob with the current crop and rotation (client-only). |
toDataUrl |
(options?: Omit<CropImageOptions, "rotation">) => Promise<string> |
Crop the loaded image into a data URL with the current crop and rotation (client-only). |
ImageCropperViewport
Source: src/families/media/image-cropper/image-cropper-viewport.vue
Props
| Prop |
Type |
Default |
Description |
wheelZoom |
boolean |
true |
Zoom around the pointer with the mouse wheel or trackpad. |
Slots
| Slot |
Slot props |
Description |
default |
ImageCropperSlotState |
Image, crop area, and overlays. Receives the cropper state. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
Behavior
Normative state x input -> outcome table for image-cropper-root.vue,
image-cropper-viewport.vue, image-cropper-image.vue, image-cropper-area.vue,
image-cropper-handle.vue, and image-cropper-grid.vue (@vizejs/ui/image-cropper).
Every row is proven by the named test.
Crops are { x, y, width, height } in the rotated image's bounding box, measured
in natural image pixels, so cropImage and server-side croppers can reproduce
the result. The viewport shows the bounds with contain scale times zoom;
geometry is published only after the image and viewport are measured, so server
and hydration markup agree.
| ID |
State |
Input |
Outcome |
Evidence |
| IC1 |
unmeasured |
image load |
the centered autoCropArea crop is requested (init); image and area receive measured positions; the area is a labelled focusable group |
publishes the initial centered crop and measured geometry once the image loads |
| IC2 |
ready |
drag the area |
the crop moves by pointer delta / scale, stays in bounds, reports moving, and emits cropEnd on release; secondary buttons are ignored |
dragging the area moves the crop in image pixels and commits on release |
| IC3 |
ready |
drag a handle |
the opposite edge stays anchored; locked aspect ratios resize symmetrically around the crop center |
handles resize from their edge or corner, including aspect-locked crops |
| IC4 |
focused area |
arrows / Alt+arrows / + - / [ ] |
arrows nudge by nudgeStep (Shift ×10), Alt/Ctrl/Meta+arrows resize, +/- zoom around the crop center, [/] rotate by rotationStep |
keyboard nudges, resizes, zooms, and rotates the focused crop area |
| IC5 |
ready |
wheel / background drag |
the wheel zooms around the pointer; dragging outside the area pans the zoomed view within the image |
wheel zoom keeps the pointer anchor and background drags pan the zoomed view |
| IC6 |
ready |
rotation change |
bounds swap on right angles and the crop keeps its relative center, clamped into the new bounds (rotate) |
rotation swaps the bounds and re-fits the crop around its relative center |
| IC7 |
controlled |
crop / zoom / rotation requests |
requests are emitted while controlled values render; zoom clamps to minZoom..maxZoom; controlled crops are not re-initialized |
controlled crop, zoom, and rotation win until the parent accepts them |
| IC8 |
any |
reset() / messages / disabled |
reset restores zoom, rotation, and the default crop; messages localize the accessible text; disabled croppers ignore every input |
reset, localized messages, and disabled croppers |
| IC9 |
image |
unsafe / cached / replaced source |
unsafe sources are dropped, cached images are read on mount, and a different image size re-initializes the crop |
images are sanitized, read when already complete, and reset the crop when replaced |
| IC10 |
grid |
render |
divisions - 1 lines per axis expose offsets through --vize-ui-image-cropper-grid-offset |
the grid renders division lines with offsets |
| IC11 |
exposed instance |
toBlob() / toDataUrl() |
exports use the current crop and rotation through cropImage, and reject before an image loads |
exports the loaded crop through the canvas helper |
| IC12 |
missing provider |
setup |
compound parts fail closed with the shared context diagnostic |
compound parts require a matching root provider |
| IC13 |
geometry |
rotation |
rotation normalizes into [0, 360) and right angles swap bounds exactly |
normalizes rotation and computes rotated bounds exactly on right angles |
| IC14 |
geometry |
clamp |
crops clamp into bounds, size limits, and aspect ratios; bounds win over minimums |
clamps crops into bounds, size limits, and aspect ratios |
| IC15 |
geometry |
default crop |
default crops are centered by coverage and aspect ratio |
centers default crops by coverage and aspect ratio |
| IC16 |
geometry |
move |
moves stay inside the bounds |
moves crops by deltas without leaving the bounds |
| IC17 |
geometry |
free resize |
every handle resizes with an anchored opposite edge, never crossing and honoring limits |
resizes free crops from every handle with anchored opposite edges |
| IC18 |
geometry |
locked resize |
corners follow the dominant axis; edges grow symmetrically; bounds and limits cap the size |
resizes aspect-locked crops along the dominant axis and within bounds |
| IC19 |
geometry |
re-fit |
re-fitting keeps the relative center and falls back to a centered crop from empty bounds |
re-fits crops after bounds change by keeping the relative center |
| IC20 |
geometry |
view mapping |
contain scale, clamped view centers, viewport mapping, and zoom anchors are exact |
maps the view with contain scale, clamped centers, and zoom anchors |
| IC21 |
canvas helper |
crop |
the image is rotated into its bounds, cut, downscaled to maxWidth/maxHeight, and encoded as a Blob or data URL |
crops, rotates, downscales, and encodes blobs and data URLs |
| IC22 |
canvas helper |
URL source |
URL sources load with the requested CORS mode before cropping; load errors reject |
loads URL sources with CORS before cropping |
| IC23 |
canvas helper |
failure |
empty crops, missing canvases, load failures, and encode failures reject with typed ImageCropperError codes |
rejects empty crops, missing canvases, failed loads, and failed encodes with typed errors |
| IC24 |
canvas helper |
pending image |
pending image elements are awaited |
waits for pending images to load |
| IC25 |
SSR |
isolated requests |
markup is byte-identical and carries no measured geometry |
renders byte-identical cropper markup without measured geometry on the server |
| IC26 |
SSR / hydration |
hydrate |
server markup hydrates without warnings or node replacement |
hydrates cropper markup without warnings or node replacement |
| IC27 |
types |
compile |
crops, handles, reasons, interactions, export overloads, and exposes are closed and read-only |
image-cropper.types.test-d.ts |
Pinch-to-zoom gestures are not interpreted; touch users zoom with the consumer's
controls (setZoom) or the keyboard.