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

live
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.

PropTypeDefaultDescription
bindobjectTwo-way bound value. .set() fires onchange; plain assignment is silent. committed crop data (v5: value)
srcstring''image source — initial and live
widthnumber800editor area width (v5 desktop area)
heightnumber360editor area height
cropwidthnumber300crop box width = minimum size (v5: width)
cropheightnumber240crop box height = minimum size (v5: height)
resizablebooleanfalsecrop box edge resize (v5: allowResize)
controlsbooleantruebuilt-in ranges + tools + buttons bar
originalbooleanfalseinclude the source image in saved data (v5)
aspectnumber0crop aspect ratio (w/h); 0 = free
formatstring"png"export format: png
qualitynumber0.92export quality for jpeg/webp (0..1)
outputwidthnumber0export width; 0 = crop box width
outputheightnumber0export 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.