
VanillaJCrop is an image cropping library that replaces the classic JCrop jQuery plugin in pure ES6+ JavaScript.
It comes with two integration modes: a declarative <jcrop-widget> Web Component and a programmatic headless API.
Both modes handle mouse, touch, and pen input through the Pointer Events API, and the component scopes all its styles inside Shadow DOM to prevent conflicts with your page CSS.
Features:
- No external dependencies.
- A native Web Component for HTML-attribute-based configuration.
- A headless API that separates core crop logic from the rendering layer.
- Handles mouse, touch, and pen input through the Pointer Events API.
- Keyboard navigation moves selections with arrow keys and cancels with Escape.
- Aspect ratio locking constrains crop areas to any fixed ratio, such as 16:9, 4:3, or 1:1.
- Minimum and maximum dimension constraints keep selections within defined pixel bounds.
- Animated transitions move the selection box smoothly from one position to another.
- Full appearance customization through CSS custom properties on the component element.
How to use it:
1. Download the package and import it as a local module.
<script type="module">
import JCrop, { JCropWidget } from './src/index.js';
</script>2. Add the <jcrop-widget> custom element to your HTML with the src attribute pointing to your image. Constraints like ratio and min-width go directly on the element as attributes:
src(string): The URL of the image to crop.ratio(number or fraction string): The locked aspect ratio. Accepts a decimal (1.5) or a fraction string (16/9).min-width(number): The minimum allowed selection width in pixels.min-height(number): The minimum allowed selection height in pixels.max-width(number): The maximum allowed selection width in pixels.max-height(number): The maximum allowed selection height in pixels.disabled(boolean): Disables all pointer and keyboard interaction when present.
<jcrop-widget src="portrait.jpg" ratio="4/3" min-width="80"> </jcrop-widget>
<script type="module">
import { JCropWidget } from './src/index.js';
const cropper = document.querySelector('jcrop-widget');
// Fires each time the user finalizes a crop selection
cropper.addEventListener('crop-select', (e) => {
// e.detail returns coordinates in actual image pixels
const { x, y, w, h } = e.detail;
console.log(`Crop: ${w}x${h} starting at (${x}, ${y})`);
});
// Fires continuously as the user drags
cropper.addEventListener('crop-change', (e) => {
console.log('Live selection:', e.detail);
});
// Fires when the crop selection is cleared
cropper.addEventListener('crop-release', () => {
console.log('No active selection');
});
</script>3. You can also control the widget through its JavaScript API. Grab the element by reference and call methods directly:
const widget = document.querySelector('jcrop-widget');
// Read the current selection as { x, y, w, h, x2, y2 } in image coordinates
console.log(widget.value);
// Set a new selection immediately
widget.setSelection({ x: 40, y: 40, x2: 380, y2: 290 });
// Animate to a new selection; optional callback fires on completion
widget.animateTo({ x: 100, y: 80, x2: 520, y2: 400 }, () => {
console.log('Transition complete');
});
// Clear the active selection
widget.release();
// Destroy the widget and remove all event listeners
widget.destroy();4. The headless API is the right choice when you need crop logic on an existing DOM container or inside a custom canvas pipeline. Pass your element and a configuration object to the JCrop constructor. The imageWidth and imageHeight options map display coordinates back to actual image pixels, so read them from the image element rather than hardcoding.
import JCrop from './src/index.js';
// Attach JCrop to a container element
const jcrop = new JCrop(document.getElementById('crop-stage'), {
canvasWidth: 720, // display width of the rendered crop area
canvasHeight: 480, // display height of the rendered crop area
imageWidth: 2160, // actual pixel width of the source image
imageHeight: 1440, // actual pixel height of the source image
ratio: 3 / 2, // lock to a 3:2 aspect ratio
minWidth: 120,
minHeight: 80,
onChange: (coords) => {
// Fires on every drag move during selection
console.log('In progress:', coords);
},
onSelect: (coords) => {
// Fires when the user releases the pointer
console.log('Final selection:', coords);
submitCropToServer(coords);
},
onRelease: () => {
console.log('Selection cleared');
}
});
// Jump the selection to a position immediately
jcrop.setSelect({ x: 60, y: 40, x2: 420, y2: 320 });
// Animate the selection to a new position
jcrop.animateTo({ x: 100, y: 70, x2: 520, y2: 410 });
// Read the current selection in image coordinates
const imageCoords = jcrop.tellSelect();
// Read the current selection in canvas (display) coordinates
const displayCoords = jcrop.tellScaled();
// Update options at runtime — useful for toggling aspect ratio on the fly
jcrop.setOptions({ ratio: 1, minWidth: 150 });
// Clear the selection
jcrop.release();
// Tear down the instance and remove all listeners
jcrop.dispose();5. API configuration options:
canvasWidth(number): Display width of the crop area in pixels. Defaults to800.canvasHeight(number): Display height of the crop area in pixels. Defaults to600.imageWidth(number): Actual pixel width of the source image. Defaults to1600.imageHeight(number): Actual pixel height of the source image. Defaults to1200.ratio(number | null): Locked aspect ratio as a width-to-height number. Passnullfor a free selection. Defaults tonull.minWidth(number): Minimum selection width in pixels. Defaults to50.minHeight(number): Minimum selection height in pixels. Defaults to50.maxWidth(number): Maximum selection width in pixels. Defaults toInfinity.maxHeight(number): Maximum selection height in pixels. Defaults toInfinity.fadeTime(number): Duration of animated transitions in milliseconds. Defaults to400.handleWidth(number): Width of resize handles in pixels. Defaults to10.handleHeight(number): Height of resize handles in pixels. Defaults to10.onChange(function): Callback that fires on every selection change. Receives a coordinates object.onSelect(function): Callback that fires when the selection is finalized. Receives a coordinates object.onRelease(function): Callback that fires when the selection is cleared. Receives no arguments.
6. API methods.
// Set the crop selection immediately; accepts { x, y, x2, y2 } in image pixels
jcrop.setSelect({ x: 80, y: 60, x2: 480, y2: 360 });
// Animate the selection to new coordinates
// Optional second argument is a callback fired when the animation ends
jcrop.animateTo({ x: 120, y: 90, x2: 560, y2: 410 }, () => {
console.log('Animation finished');
});
// Return the current selection in image coordinates: { x, y, w, h, x2, y2 }
const imageCoords = jcrop.tellSelect();
// Return the current selection in canvas (display) coordinates
const displayCoords = jcrop.tellScaled();
// Clear the active selection
jcrop.release();
// Check whether an animation is currently running; returns a boolean
const active = jcrop.isAnimating();
// Stop any running animation at its current position
jcrop.cancelAnimation();
// Update one or more options at runtime; changes take effect immediately
jcrop.setOptions({ ratio: 16 / 9, minWidth: 200 });
// Destroy the instance and remove all event listeners from the DOM
jcrop.dispose();7. Web component events.
const widget = document.querySelector('jcrop-widget');
// Fires continuously as the user drags; useful for live coordinate previews
// e.detail: { x, y, w, h, x2, y2 } in image coordinates
widget.addEventListener('crop-change', (e) => {
console.log('Dragging:', e.detail);
});
// Fires once when the user releases the pointer and the selection is confirmed
// e.detail: { x, y, w, h, x2, y2 } in image coordinates
widget.addEventListener('crop-select', (e) => {
console.log('Confirmed crop:', e.detail);
});
// Fires when the current selection is cleared programmatically or by the user
widget.addEventListener('crop-release', () => {
console.log('Selection removed');
});
// Fires on errors; most common cause is a failed image load
// e.detail: { error }
widget.addEventListener('crop-error', (e) => {
console.error('Load failure:', e.detail.error);
});8. Customize the cropper with the following CSS variables.
jcrop-widget {
/* Translucent shade overlay outside the selection */
--jcrop-shade-color: rgba(0, 0, 0, 0.6);
/* Selection border */
--jcrop-border-color: #ffffff;
--jcrop-border-width: 2px;
--jcrop-border-style: solid;
/* Resize handle appearance */
--jcrop-handle-size: 14px;
--jcrop-handle-color: #ffffff;
--jcrop-handle-border: 2px solid #1a1a1a;
--jcrop-handle-radius: 3px;
}9. Migrating from JCrop jQuery plugin:
// ---- JCrop (jQuery) ----
$('#photo').Jcrop({
onSelect: function(c) { console.log(c); },
aspectRatio: 1.5
});
api.setSelect([80, 60, 480, 360]); // array format
api.animateTo([120, 90, 560, 410]); // array format
api.tellSelect();
api.release();
api.destroy();
// ---- VanillaJCrop (ES6+) ----
const jcrop = new JCrop(document.getElementById('photo'), {
onSelect: (c) => console.log(c),
ratio: 1.5
});
jcrop.setSelect({ x: 80, y: 60, x2: 480, y2: 360 }); // object format
jcrop.animateTo({ x: 120, y: 90, x2: 560, y2: 410 }); // object format
jcrop.tellSelect();
jcrop.release();
jcrop.dispose(); // renamed from destroy()






