Image Cropper
Pick an image, crop it in a dialog, and get the result back as a URL or File.
Built on svelte-easy-crop. The trigger opens
the file picker, the dialog hosts the cropper, and Crop writes the result back to src.
Installation
Installs the style saved in lily.json. For a new Diamond project,
run lily-svelte init --style diamond first.
Install the lily base and `utils` (run once per project).
npx lily-svelte@latest init Install svelte-easy-crop, then copy the image-cropper source into $lib/components/ui/image-cropper.
Usage
<script lang="ts">
import * as ImageCropper from '$lib/components/ui/image-cropper';
let src = $state('/avatar.png');
</script>
<ImageCropper.Root bind:src>
<ImageCropper.UploadTrigger>
<ImageCropper.Preview />
</ImageCropper.UploadTrigger>
<ImageCropper.Dialog>
<ImageCropper.Cropper />
<ImageCropper.Zoom />
<ImageCropper.Controls>
<ImageCropper.Cancel />
<ImageCropper.Crop />
</ImageCropper.Controls>
</ImageCropper.Dialog>
</ImageCropper.Root> ImageCropper.Root
├── ImageCropper.UploadTrigger
│ └── ImageCropper.Preview
└── ImageCropper.Dialog
├── ImageCropper.Cropper
├── ImageCropper.Zoom
└── ImageCropper.Controls
├── ImageCropper.Cancel
└── ImageCropper.Crop Root renders the hidden <input type="file">; extra attributes such as accept or name go straight onto it. UploadTrigger is a <label> for that input, so anything inside it opens
the picker.
Getting the result
src is updated with an object URL after each crop, and onCropped fires with the same URL.
To submit it, turn the URL into a File.
<script lang="ts">
import * as ImageCropper from '$lib/components/ui/image-cropper';
import { getFileFromUrl } from '$lib/components/ui/image-cropper';
async function upload(url: string) {
const file = await getFileFromUrl(url, 'avatar.png');
const body = new FormData();
body.append('avatar', file);
await fetch('/api/avatar', { method: 'POST', body });
}
</script>
<ImageCropper.Root onCropped={upload}>…</ImageCropper.Root> Files whose type is not a browser-renderable image are rejected through onUnsupportedFile, and
a canvas that fails to draw reports through onCropError.
While the crop is drawing, the Crop button shows a spinner and Cancel is disabled, so a second click cannot crop the same file twice. Picking a new file mid-crop discards the stale result.
Shape and preview
Cropper takes every svelte-easy-crop prop — cropShape, aspect, showGrid, minZoom, maxZoom… Preview accepts a child snippet to draw the current src however you like.
Zoom
Zoom is a Slider bound to the cropper's zoom, alongside pinch and wheel. Leave it out if you
only want gestures.
min and max default to 1 and 3 to match svelte-easy-crop's own minZoom / maxZoom. If you
change those on Cropper, change them here too, or the slider will stop short of the range the
cropper allows.
<ImageCropper.Cropper maxZoom={5} />
<ImageCropper.Zoom max={5} /> Object URLs
Cropping produces an object URL and assigns it to src. The cropper revokes the file it was
working from, and revokes an earlier cropped url when a new crop replaces it — but the latest one is left alive, because src still points at it. Revoke it yourself once you are done, e.g.
after uploading.