Skip to content
Inputs

Image Cropper

Crop an image by moving the image, not the box.

Walkthrough

1. Basic

The window stands still and the picture moves under it — drag it, scroll to zoom, pinch on a touchscreen. It is constrained, so an empty corner inside the frame is not a thing that can happen.

Sunset over hills

Loading…

<script lang="ts">	import { ImageCropper } from 'omaris';​	/** A drawn picture, so the demo needs no network. */	const PHOTO = `data:image/svg+xml;utf8,${encodeURIComponent(`		<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800">			<defs>				<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">					<stop offset="0%" stop-color="#1b3a5c"/>					<stop offset="60%" stop-color="#e9825c"/>					<stop offset="100%" stop-color="#f3c98b"/>				</linearGradient>			</defs>			<rect width="1200" height="800" fill="url(#sky)"/>			<circle cx="820" cy="300" r="90" fill="#fff3d6"/>			<path d="M0 620 L260 460 L470 620 Z" fill="#12283d"/>			<path d="M330 620 L620 400 L900 620 Z" fill="#0d1f30"/>			<rect y="620" width="1200" height="180" fill="#08151f"/>		</svg>`)}`;</script>​<ImageCropper src={PHOTO} alt="Sunset over hills" aspect={16 / 9} class="w-full max-w-lg" />

2. Ratios

ratios puts a row of aspect presets under the stage and rebinds aspect — pick one and the window reshapes, the picture re-covers it, nothing else moves. resizable adds grips to the window, so a locked ratio can still be scaled and a free one shaped by hand.

Sunset over hills

Loading…

aspect: 1.78
<script lang="ts">	import { ImageCropper, Text, type ImageCropperAspect } from 'omaris';​	const PHOTO = `data:image/svg+xml;utf8,${encodeURIComponent(`		<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800">			<rect width="1200" height="800" fill="#12233b"/>			<circle cx="880" cy="240" r="110" fill="#fbbf24"/>			<path d="M0 640 L300 420 L560 640 Z" fill="#1f4d3f"/>			<path d="M380 640 L700 380 L1000 640 Z" fill="#14332b"/>			<rect y="640" width="1200" height="160" fill="#0a1a26"/>		</svg>`)}`;​	let aspect = $state<ImageCropperAspect>(16 / 9);</script>​<div class="flex w-full max-w-lg flex-col gap-2">	<ImageCropper		bind:aspect		src={PHOTO}		alt="Sunset over hills"		ratios		resizable		height={260}		controls={['zoom', 'reset']}	/>​	<Text variant="label-sm" tone="muted" font="mono">		aspect: {aspect === 'free' ? 'free' : aspect.toFixed(2)}	</Text></div>

3. Exporting

The export replays exactly the transform you see, at whatever output size you name — so what lands in the blob is what was in the frame. onchange reports the rectangle in source pixels every time the crop settles.

Avatar source

Loading…

<script lang="ts">	import { Button, ImageCropper, Text, type CropRect } from 'omaris';​	const PHOTO = `data:image/svg+xml;utf8,${encodeURIComponent(`		<svg xmlns="http://www.w3.org/2000/svg" width="900" height="900">			<rect width="900" height="900" fill="#0f172a"/>			<circle cx="450" cy="380" r="180" fill="#38bdf8"/>			<rect x="180" y="600" width="540" height="180" rx="40" fill="#f472b6"/>		</svg>`)}`;​	let cropper = $state<ImageCropper | null>(null);	let output = $state<string | null>(null);	let rect = $state<CropRect | null>(null);</script>​<div class="flex w-full flex-col items-center gap-3">	<ImageCropper		bind:this={cropper}		src={PHOTO}		alt="Avatar source"		aspect={1}		shape="circle"		onchange={(next) => (rect = next)}		class="w-full max-w-sm"	/>​	<div class="flex items-center gap-3">		<Button			size="sm"			onclick={() => (output = cropper?.toDataURL('image/png', undefined, 160) ?? null)}		>			Export at 160px		</Button>		{#if output}			<img src={output} alt="The exported crop" class="size-16 rounded-full" />		{/if}	</div>​	{#if rect}		<Text variant="label-sm" tone="muted" font="mono">			{Math.round(rect.width)} × {Math.round(rect.height)} at {Math.round(rect.x)}, {Math.round(				rect.y			)}		</Text>	{/if}</div>

4. Adjusting

controls takes the list you want, so straighten adds a fine-angle slider next to the quarter-turn button. Any angle constrains as tightly as a right one: tilt the picture and it grows to keep the window full.

Two blocks on a horizon

Loading…

0°
<script lang="ts">	import { ImageCropper, Text } from 'omaris';​	const PHOTO = `data:image/svg+xml;utf8,${encodeURIComponent(`		<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="700">			<rect width="1000" height="700" fill="#0f172a"/>			<rect x="80" y="120" width="380" height="460" rx="24" fill="#38bdf8"/>			<rect x="540" y="240" width="380" height="340" rx="24" fill="#f472b6"/>			<path d="M0 620 L1000 560" stroke="#e2e8f0" stroke-width="8"/>		</svg>`)}`;​	let rotation = $state(0);	let flipX = $state(false);</script>​<div class="flex w-full max-w-md flex-col gap-2">	<ImageCropper		bind:rotation		bind:flipX		src={PHOTO}		alt="Two blocks on a horizon"		aspect={4 / 3}		height={240}		controls={['zoom', 'straighten', 'rotate', 'flip', 'reset']}	/>​	<Text variant="label-sm" tone="muted" font="mono">		{Math.round(rotation)}°{flipX ? ' · mirrored' : ''}	</Text></div>

5. Overridden

height is a custom property, so classes.stage can replace it outright. controls={false} hands the zoom to a slider of your own through the bindable zoom, and children puts content over the stage.

Hills

Loading…

Cover photo
Zoom
<script lang="ts">	import { ImageCropper, Slider, Text } from 'omaris';​	const PHOTO = `data:image/svg+xml;utf8,${encodeURIComponent(`		<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="600">			<rect width="1000" height="600" fill="#0b3d2e"/>			<circle cx="300" cy="220" r="120" fill="#facc15"/>			<path d="M0 520 L340 300 L700 520 Z" fill="#065f46"/>			<path d="M420 520 L760 340 L1000 520 Z" fill="#064e3b"/>		</svg>`)}`;​	let zoom = $state(1.4);</script>​<div class="flex w-full max-w-md flex-col gap-3">	<ImageCropper		src={PHOTO}		alt="Hills"		aspect={3 / 1}		bind:zoom		grid={false}		controls={false}		inset={0.94}		height="auto"		classes={{ stage: 'h-40 rounded-shape-lg', window: 'rounded-shape-md' }}	>		<Text variant="label-sm" class="absolute start-3 top-3 text-white/80">Cover photo</Text>	</ImageCropper>​	<Slider bind:value={zoom} min={1} max={4} step={0.05} label="Zoom" /></div>