Crop an image before upload. One function, no dependencies, returns a Blob. Uses your modal system or brings its own.
const result = await quickcrop(file, { aspect: 1, maxWidth: 512 });
// result: { blob, dataURL, width, height } on confirm, null on cancel<script type="module">
import quickcrop from 'https://cdn.jsdelivr.net/npm/quickcrop@1/quickcrop.js';
</script>The script injects its own styles. To theme via CSS instead, link the stylesheet (the script then skips injection):
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/quickcrop@1/quickcrop.css">Those URLs are served straight from npm by jsDelivr, no setup. The @1 pin tracks the latest 1.x release, so you get patches and minor updates but never a breaking major. Pin exactly with quickcrop@1.2.1 to freeze a version, or drop the pin (.../npm/quickcrop/quickcrop.js) to ride the newest major. The same files are on unpkg too: https://unpkg.com/quickcrop@1/quickcrop.js.
npm install quickcropimport quickcrop from 'quickcrop';
// optional, only if you want to theme via CSS:
import 'quickcrop/quickcrop.css';<input type="file" id="avatar" accept="image/*">
<script type="module">
import quickcrop from 'https://cdn.jsdelivr.net/npm/quickcrop@1/quickcrop.js';
document.getElementById('avatar').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
e.target.value = ''; // allow re-selecting the same file
const result = await quickcrop(file, { aspect: 1, maxWidth: 512 });
if (!result) return; // cancelled
const form = new FormData();
form.append('avatar', result.blob, 'avatar' + (result.blob.type === 'image/png' ? '.png' : '.jpg'));
await fetch('/upload', { method: 'POST', body: form });
});
</script>The user picks a file, a crop modal opens, they drag/resize the crop box (rule-of-thirds grid, corner handles, aspect lock if you set one), and confirm. You get back a Blob ready for FormData.
quickcrop() returns a Promise that resolves to:
{ blob, dataURL, width, height }when the user confirms.blobis the cropped image,dataURLis the same image as a data URL (handy for instant previews),width/heightare the output dimensions in pixels.nullwhen the user cancels (Escape, backdrop click, or themodal's close button when themodal hosts).
It rejects on a non-File/Blob argument, an undecodable image, or a second call while a cropper is already open.
| Option | Default | Meaning |
|---|---|---|
aspect |
null |
Width / height lock. null is a free crop. 1 is square, 16/9, 4/3, etc. |
type |
smart | Output mime type. Defaults to the input file's type when it is image/jpeg, image/png, or image/webp, otherwise image/png. A photo stays a jpeg instead of ballooning into a png. |
quality |
0.92 |
Encoder quality (0 to 1) for jpeg/webp. Ignored for png. |
maxWidth |
null |
Cap the output width in pixels; the crop is downscaled proportionally. The upload-size lever. |
maxHeight |
null |
Same for height. When both are set, the stricter one wins. |
minSize |
40 |
Minimum crop box edge in display pixels. |
labels |
{ confirm: 'Crop' } |
Confirm button text. |
modal |
'auto' |
'auto', 'builtin', a themodal instance, or a custom adapter (below). |
quickcrop separates the crop stage from the modal that hosts it.
'auto'(default): ifwindow.themodal(from hyperclayjs) is present, quickcrop renders inside it, so a Hyperclay page gets platform-consistent modals for free. Otherwise the built-in modal is used.'builtin': always use the built-in modal, even when themodal is present.- A themodal instance:
quickcrop(file, { modal: themodal })for ESM users who import themodal directly without globals. - A custom adapter: plug in any modal system by normalizing it to one function:
const myAdapter = {
open({ content, confirmLabel, onConfirm, onCancel }) {
// content is a live HTMLElement (the crop stage); put it in your modal.
// Call onConfirm() when the user accepts, onCancel() when they dismiss
// (Esc, backdrop, a close button: dismissal affordances are yours).
// Only call them from user interaction, never during open() itself.
const dialog = document.createElement('dialog');
dialog.append(content);
// ... your confirm button wired to onConfirm, dismissal to onCancel ...
document.body.append(dialog);
dialog.showModal();
return { close() { dialog.close(); dialog.remove(); } };
},
// optional: tell quickcrop how much room your modal has for the image
fit() {
return { width: window.innerWidth * 0.8, height: window.innerHeight * 0.7 };
},
};
await quickcrop(file, { modal: myAdapter });The default look is the warm "pixel quiet" palette. Override any of the CSS variables (set them after the stylesheet loads, or just link your own quickcrop.css):
:root {
--qc-surface: #17191d;
--qc-surface-hover: #1f242b;
--qc-border: #2a2e35;
--qc-text: #e7e9ee;
--qc-on-dark: #0f1012;
}| Variable | Default | Used for |
|---|---|---|
--qc-surface |
#f7f2ea |
Modal and button background |
--qc-surface-hover |
#efe7d8 |
Button hover |
--qc-border |
#cdbfa6 |
Modal and button borders |
--qc-text |
#2b241b |
Text and the primary button background |
--qc-text-hover |
#463c2e |
Primary button hover |
--qc-on-dark |
#efe7d8 |
Text on the primary button |
--qc-overlay |
rgba(43,36,27,.6) |
Backdrop |
--qc-dim |
rgba(0,0,0,.55) |
Dimming outside the crop box |
--qc-radius |
8px |
Modal corner radius |
--qc-crop-line |
#fff |
Crop box outline, grid, and handles |
--qc-font |
ui-sans-serif, system-ui, sans-serif |
Modal font |
All modern browsers, mouse and touch. Needs ES modules, pointer events, and canvas.toBlob, which everything from 2020 onward has. EXIF orientation in photos is handled by the browser itself (image-orientation: from-image is the default everywhere now).
Device notes:
- Output is capped at ~16.7 million pixels, the canvas area iOS Safari can reliably encode; larger crops are downscaled proportionally. Use
maxWidth/maxHeightto cap lower. - The realized format is
result.blob.type: engines without a webp canvas encoder (older Safari) fall back to png per spec. - Browsers generally cannot decode HEIC/HEIF in
<img>, so quickcrop rejects those files withcould not decode image. In practice iOS file inputs transcode camera-roll HEIC to jpeg on selection, so upload flows rarely see one.
npm run build # syncs version + embedded CSS into quickcrop.js, README.md, llms.txt
npm test # Playwright specs in test/
npm publish # publishes quickcrop.js + quickcrop.css (runs build automatically)MIT-0 (MIT No Attribution).