Try it
A real form, a real upload
Drop or pick an image, or click one of the samples. Toggle mode, shape, output format and quality — the Submit button below reports what the server would actually receive.
Form payload
Interactive crop
Drag the frame, type the coords
Opt in with data-crop="interactive" (fill mode only). The source image shows in the stage with a draggable crop frame — pan with the body, resize from the corners (aspect locked to target), or type exact pixel coordinates in the toolbar.
Button UI
When a drop zone is overkill
Set data-ui="button" for a compact, accented button instead of the full-width drop zone. Same pipeline, same toolbar after a file is picked.
Install & usage
Drop it in
One stylesheet, one script, one attribute. Auto-mounts on DOMContentLoaded.
<link rel="stylesheet" href="path/to/croppy.css">
<script src="path/to/croppy.js"></script>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="avatar"
data-croppy
data-ui="dropzone" <!-- dropzone (default) | button -->
data-mode="fill" <!-- fill | fit | resize -->
data-shape="square" <!-- square | portrait | landscape -->
data-crop="interactive" <!-- center (default) | interactive (fill only) -->
data-format="image/jpeg" <!-- image/png | image/jpeg | image/webp -->
data-quality="0.85" <!-- 0..1, lossy formats only -->
data-samples='[{"src":"/img/a.jpg","name":"A"}]'
accept="image/*">
<button>Save</button>
</form>
The original <input> stays in the DOM; its files list is replaced at pick time, so the form posts the processed image under the input's original name. No custom submit handler needed.
Modes
Fill, fit, resize
Same source image, three different jobs.
Fill
Cover the box. Crops what doesn't fit. Result = exact target size.
Fit
Contain inside the box. Bars fill the rest in the plugin's accent color.
Resize
Scale to fit under the max. No bars, no crop — output size matches the image.
JavaScript API
For when auto-init isn't enough
// Manual init
Croppy.initAll(scope);
Croppy.init(inputElement);
// Drive an instance
const inst = Croppy.instances.get(inputElement);
inst.setMode('fit'); // 'fill' | 'fit' | 'resize'
inst.setShape('landscape'); // 'square' | 'portrait' | 'landscape'
inst.setFormat('image/webp'); // 'image/png' | 'image/jpeg' | 'image/webp'
inst.quality = 0.7; inst.render(); // re-encode with new quality
inst.loadSample({ src: '/img/x.jpg', name: 'X' });
// Read the current crop rect (interactive fill only)
inst.crop; // { x, y, w, h } in source-pixel coords, or null
On every render the input's files list is replaced with a single File holding the processed image, so any normal form submit (or new FormData(form)) carries the cropped + compressed result.