---
title: "JavaScript Image Cropper"
description: "The LemonadeJS Image Cropper block for JavaScript: Image cropping with zoom and rotate. Contract-verified, framework-agnostic, zero dependencies, with a live example."
source: https://lemonadejs.com/docs/plugins/cropper/
---

<link rel="stylesheet" href="/v6/cropper.css">

# 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

<!--example-->

```js
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

```bash
npm install @lemonadejs/cropper
```

```js
import Cropper from '@lemonadejs/cropper';
import '@lemonadejs/cropper/style.css';
```

Three deployment forms, one component:

```js
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 | jpeg | webp |
| `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

```js
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:

```js
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](/docs/v5/plugins/).