Custom stencil

This tutorial builds a circle that moves by dragging its body and resizes around its centre from a single control. Its implementation uses public geometry and gesture primitives.

handlerstencilbounding boxoverlay
A round stencil with a single resize handler in its top-right part

Register the stencil

Call getCropperContext() during component initialization. Register a getter before checking whether cropper state exists. The getter supplies aspectRatio: 1 and BoundingBoxType.Circle; the cropper combines those options with its constraint strategy before the first reset. Registration automatically cleans up on destruction.

The context exposes a stable cropper API, reactive disabled, and the currently displayed image. Read changing values with $derived. An enclosing cropper’s disabled state must reach every gesture surface.

Position and draw

getStencilCoordinates() supplies layout coordinates to StencilWrapper; getTransitions() keeps its movement aligned with the image. StencilOverlay shades outside the circle. The demo’s CSS supplies the round outline and positions the resize control.

the centre stays in place
Dragging the handler grows the circle in every direction at once

Move and resize

Two DraggableArea instances handle the body and the resize control. They install native non-passive listeners through attachments. The body forwards only the displacement to moveCoordinates; forwarding the native event as its second argument would incorrectly put it in the method’s options position.

shift.leftleftrighttopbottom
One handler shift moves all four edges of the bounding box by the same amount

The resize callback uses the center anchor and horizontal displacement for both dimensions. Moving right grows the circle evenly; moving left shrinks it. The registered ratio keeps it square.

<script lang="ts">
	import { MoveDiagonal } from '@lucide/svelte';
	import {
		DraggableArea,
		getCropperContext,
		StencilOverlay,
		StencilWrapper,
		type MoveDirections
	} from 'svelte-advanced-cropper';

	import { BoundingBoxType } from 'advanced-cropper/extensions/fit-to-image';

	const context = getCropperContext();
	const cropper = context.cropper;
	context.registerStencil(() => ({ aspectRatio: 1, boundingBox: BoundingBoxType.Circle }));
	const disabled = $derived(context.disabled);
	const state = $derived(cropper.getState());

	const coordinates = $derived(cropper.getStencilCoordinates());
	const transitions = $derived(cropper.getTransitions());

	function resize(shift: MoveDirections) {
		// Only the horizontal shift is used, on both axes: the square stencil grows evenly.
		cropper.resizeCoordinates('center', {
			left: shift.left,
			top: shift.left
		});
	}
</script>

{#if state}
	<StencilWrapper class="circle-stencil" {transitions} {...coordinates}>
		<DraggableArea
			{disabled}
			class="circle-stencil__handler"
			onMove={resize}
			onMoveEnd={cropper.resizeCoordinatesEnd}
		>
			<MoveDiagonal color="white" size={22} />
		</DraggableArea>
		<DraggableArea
			{disabled}
			class="circle-stencil__draggable-area"
			onMove={(directions) => cropper.moveCoordinates(directions)}
			onMoveEnd={cropper.moveCoordinatesEnd}
		>
			<StencilOverlay class="circle-stencil__overlay" />
		</DraggableArea>
	</StencilWrapper>
{/if}

<style>
	:global(.circle-stencil) {
		cursor: move;
	}
	:global(.circle-stencil__handler) {
		position: absolute;
		right: 15%;
		top: 14%;
		z-index: 1;
		cursor: ne-resize;
		width: 30px;
		height: 30px;
		display: flex;
		align-items: center;
		justify-content: center;
		transform: translate(50%, -50%);
	}
	:global(.circle-stencil__overlay) {
		border: dashed 2px white;
		box-sizing: border-box;
		border-radius: 50%;
	}
	:global(.circle-stencil__draggable-area) {
		width: 100%;
		height: 100%;
	}
</style>

Each gesture calls its matching completion method: moveCoordinatesEnd for the body and resizeCoordinatesEnd for the handle. These end interaction state and allow pending reconciliation to run. When composing BoundingBox controls instead, preserve the native event in onMove forwarding so modifier-key behaviour survives.

Use it as a child

Drag the circle, then its diagonal control. The cropper can configure core restrictions normally; the custom stencil owns only its presentation and interaction rules. See stencil context for lifecycle details and Print guides for reactive, product-specific options.