Skip to content
Devix Open Source

Guide

Getting started

npm install @devix-labs/image-cropper
<label for="avatar">Profile picture</label>
<input id="avatar" name="avatar" type="file">
import { createImageCropper } from '@devix-labs/image-cropper';
import '@devix-labs/image-cropper/styles.css';

const picker = createImageCropper(document.querySelector('#avatar'), {
  aspect: 1,
  mode: 'fixed',
  round: true,
  maxWidth: 512,
});

// The part every other cropper leaves to you:
const file = await picker.toFile();   // a real File, cropped, 512px, named after the original

The input you point at stays the control. It is already focusable, already announced by a screen reader, and already a form field; the drop zone is its label.

Getting the picture out

This is the whole point. Three ways, all giving you the pixels you actually selected, at the resolution of the original rather than of the screen:

await picker.toBlob();                                   // a Blob
await picker.toFile();                                   // a File, named after the original
await picker.toFile('avatar', { type: 'image/jpeg' });   // "avatar.jpg"
await picker.toDataURL();                                // a data: URL

Every option can be overridden per call, so one picker can produce a thumbnail and a full-size copy:

const thumb = await picker.toFile('thumb', { maxWidth: 128, type: 'image/webp', quality: 0.8 });
const full = await picker.toFile('photo', { maxWidth: 2000, type: 'image/jpeg', quality: 0.9 });

Sending it

const body = new FormData();
body.append('avatar', await picker.toFile());
await fetch('/profile/avatar', { method: 'POST', body });

Or hand it straight to the file uploader:

uploader.addFiles([await picker.toFile()]);

Shapes

createImageCropper(input, { aspect: 16 / 9 });      // a fixed shape
createImageCropper(input, { aspect: null });        // free
createImageCropper(input, { aspect: [0.5, 2] });    // anywhere between 1:2 and 2:1

A range is the thing people keep asking other croppers for: it leaves the selection exactly as drawn while it is between the two, and only holds at the ends.

Offer the choice as buttons:

createImageCropper(input, {
  aspects: [
    { label: 'Free', value: null },
    { label: 'Square', value: 1 },
    { label: 'Wide', value: 16 / 9 },
    { label: 'Tall', value: 4 / 5 },
  ],
});

Two ways to crop

mode: 'free' (the default) — the picture stays put and the crop is dragged and resized over it. This is what you want when the whole image matters and the crop is a choice.

mode: 'fixed' — the crop is a hole in the middle and the picture moves under it. This is the avatar case: with round: true it is the circle every profile form needs, and the picture can never pull away from behind it.

An output size that is honest

createImageCropper(input, { maxWidth: 1600, maxHeight: 1600, type: 'image/webp', quality: 0.82 });

maxWidth and maxHeight only ever scale down: enlarging a crop invents detail that was never there. width/height force an exact size when a design demands one. The widget shows the pixel size you are about to get, under the stage, while you drag.

With no type, a PNG stays a PNG — so a picture chosen for its transparency keeps it — and anything else becomes WebP where the browser can write it.

Where to go next

Updated 15 Sep 2026