JavaScript Image Cropper
@lemonadejs/cropper · ✓ 31 contract checks · framework-agnostic · zero dependencies
<Cropper /> — a quick image editor (crop · transform · adjust · filter · export)
Evolved from the v5 @jsuites/cropper engine, which v6 vendored inline (zero runtime deps). The pan / wheel-zoom / crop-box geometry is the v5 math verbatim; everything else is modernised:
- ADJUST + FILTER run on the native CanvasRenderingContext2D.filter (GPU-accelerated) instead of the v5 per-pixel getImageData loops: brightness, contrast, saturation, hue, blur, grayscale, sepia, invert — one filter string, baked into the canvas so the crop export picks them up for free (prefer the platform over JS emulation)
- TRANSFORM: continuous rotate (v5 [-1..1] → ±180°) plus 90° steps and horizontal/vertical flip, composed in one center transform
- CROP box: drag to move, resize from the 8 edges/corners when resizable, with an optional ASPECT-RATIO lock (free / 1:1 / 16:9 / custom) that constrains the box as it resizes
- LOAD: file picker (click when empty, double click, Upload button, api.upload()), drag-and-drop, the src prop (live), api.setValue()
- EXPORT: save() reads the box pixels into a dataURL with an optional output format (png/jpeg/webp), quality and output size, and commits { file, content, extension(, original) } — the v5 value shape — to the bound state, firing onchange
v5 → v6 mapping is unchanged from the original port (value → bind, options.area → width/height, wrapper size → cropwidth/cropheight, allowResize → resizable, range controls + buttons → the controls bar). New props/api are purely additive; the old contract still holds.
jsdom has no canvas: a null 2d context downgrades drawing to a no-op.
Example
import { html } from 'lemonadejs';
import Cropper from '@lemonadejs/cropper';
// A sample picture as an inline SVG (same-origin, so the crop can be exported)
const sample = 'data:image/svg+xml;utf8,' + encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600">
<defs><linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#0ea5e9"/><stop offset="1" stop-color="#fde68a"/></linearGradient></defs>
<rect width="900" height="380" fill="url(#sky)"/><circle cx="660" cy="300" r="70" fill="#fbbf24"/>
<rect y="380" width="900" height="220" fill="#16a34a"/>
<path d="M60 420 L100 330 L140 420 M260 420 L300 330 L340 420 M460 420 L500 330 L540 420" fill="#166534"/>
</svg>`);
const App = (props, { state }) => {
const photo = state(null);
return html`<div>
<${Cropper} bind="${photo}" src="${sample}" width="${600}" height="${300}" cropwidth="${240}" cropheight="${160}" resizable />
<p style="font-size:13px">Drag to pan, scroll to zoom, then press Save (or play with the sliders).</p>
${() => photo.value && html`<p>Saved crop:<br /><img src="${() => photo.value.content}" alt="cropped export" style="border:1px solid #cbd5e1;border-radius:6px" /></p>`}
</div>`;
};Installation
npm install @lemonadejs/cropper
import Cropper from '@lemonadejs/cropper';
import '@lemonadejs/cropper/style.css';
Three deployment forms, one component:
html`<${Cropper} />` // by value (no registration)
setComponents({ Cropper }); // then <Cropper /> by name anywhere
createWebComponent(Cropper); // <lm-cropper> in plain HTML/any framework
Props
Every declared prop arrives as a live state — pass a value for a snapshot or a state for a two-way live wire. Attribute strings are coerced to the declared type.
| Prop | Type | Default | Description |
|---|---|---|---|
bind | object | — | Two-way bound value. .set() fires onchange; plain assignment is silent. committed crop data (v5: value) |
src | string | '' | image source — initial and live |
width | number | 800 | editor area width (v5 desktop area) |
height | number | 360 | editor area height |
cropwidth | number | 300 | crop box width = minimum size (v5: width) |
cropheight | number | 240 | crop box height = minimum size (v5: height) |
resizable | boolean | false | crop box edge resize (v5: allowResize) |
controls | boolean | true | built-in ranges + tools + buttons bar |
original | boolean | false | include the source image in saved data (v5) |
aspect | number | 0 | crop aspect ratio (w/h); 0 = free |
format | string | "png" | export format: png |
quality | number | 0.92 | export quality for jpeg/webp (0..1) |
outputwidth | number | 0 | export width; 0 = crop box width |
outputheight | number | 0 | export height; 0 = crop box height |
Events
All event names are lowercase (the platform convention — LJS-305 warns otherwise).
onchange— fires when crop data commits (save/delete/setValue)onload— fires when an image lands in the editor
API
import { ref } from 'lemonadejs';
const cropper = ref();
html`<${Cropper} ref="${cropper}" />`;
// cropper.current.getValue(...) · cropper.current.setValue(...) · cropper.current.getImage(...) · cropper.current.zoom(...) · cropper.current.rotate(...) · cropper.current.brightness(...) · cropper.current.contrast(...) · cropper.current.saturate(...) · cropper.current.grayscale(...) · cropper.current.sepia(...) · cropper.current.hue(...) · cropper.current.blur(...) · cropper.current.invert(...) · cropper.current.rotateLeft(...) · cropper.current.rotateRight(...) · cropper.current.flipHorizontal(...) · cropper.current.flipVertical(...) · cropper.current.setAspect(...) · cropper.current.save(...) · cropper.current.reset(...) · cropper.current.upload(...)
getValue()setValue()getImage()zoom()rotate()brightness()contrast()saturate()grayscale()sepia()hue()blur()invert()rotateLeft()rotateRight()flipHorizontal()flipVertical()setAspect()save()reset()upload()
Styling
All classes follow the lm-cropper-* convention; visual variants are data-*
attributes on the root. Override freely — there is no styling engine to fight.
Contract
The machine-readable schema ships with the package:
import contract from '@lemonadejs/cropper/contract.json';
verify.json carries the conformance proof produced by verify(Cropper).
Looking for the v5 plugin? See the archived v5 documentation.