Replace layers with snippets

A layer snippet receives the current cropper, positioning classes and the services that layer needs. Spread its argument into the default building block, then add your own controls. This example wraps image gestures with a scrolling notification. The snippet contracts list every field.

<script lang="ts">
	import { onDestroy } from 'svelte';
	import {
		CropperBackgroundWrapper,
		isTouchEvent,
		isWheelEvent,
		type CropperBackgroundWrapperSnippetProps,
		type TransformableImageEvent
	} from 'svelte-advanced-cropper';

	let { children, ...props }: CropperBackgroundWrapperSnippetProps = $props();

	type NotificationType = 'touch' | 'wheel';

	const messages: Record<NotificationType, string> = {
		touch: 'Use two fingers to move the cropper',
		wheel: 'Use ctrl + scroll to zoom the cropper'
	};

	let notificationType: NotificationType = $state('wheel');
	let notificationVisible = $state(false);

	// A tiny debounce: hide the notification 1.5s after the last event that showed it.
	let hideTimer: ReturnType<typeof setTimeout> | undefined;

	onDestroy(() => clearTimeout(hideTimer));

	function hideNotificationLater() {
		clearTimeout(hideTimer);
		hideTimer = setTimeout(() => (notificationVisible = false), 1500);
	}

	function showNotification(type: NotificationType) {
		notificationVisible = true;
		notificationType = type;
		hideNotificationLater();
	}

	// Let a one-finger swipe or a plain wheel scroll the page. Only two fingers or
	// ctrl + wheel transform the image.
	function onEvent(event: TransformableImageEvent, nativeEvent: Event) {
		if (isTouchEvent(nativeEvent)) {
			if (nativeEvent.touches.length === 1 && !event.active) {
				showNotification('touch');
				event.preventDefault();
			} else {
				notificationVisible = false;
			}
		} else if (isWheelEvent(nativeEvent)) {
			if (!event.active && !nativeEvent.ctrlKey) {
				showNotification('wheel');
				event.preventDefault();
			} else {
				notificationVisible = false;
			}
		}

		// Events the image handles must not also scroll the page.
		if (!event.defaultPrevented) {
			nativeEvent.preventDefault();
			nativeEvent.stopPropagation();
		}
	}
</script>

<CropperBackgroundWrapper {...props} {onEvent}>
	{@render children?.()}
	<div
		class={[
			'cropper-event-notification',
			notificationVisible && 'cropper-event-notification--visible'
		]}
	>
		{messages[notificationType]}
	</div>
</CropperBackgroundWrapper>

<style>
	.cropper-event-notification {
		position: absolute;
		inset: 0;
		display: flex;
		align-items: center;
		justify-content: center;
		padding: 0 50px;
		color: white;
		font-size: 20px;
		text-align: center;
		background: rgba(0, 0, 0, 0.6);
		opacity: 0;
		pointer-events: none;
		transition: opacity 1s;
	}
	.cropper-event-notification--visible {
		opacity: 1;
		pointer-events: all;
		transition: opacity 0.25s;
	}
</style>
Wrapper
Background image
Wrapper
wrapper snippet; cropper class and style
Boundary
boundary snippet; .advanced-cropper__boundary
Image gestures
backgroundWrapper snippet; .advanced-cropper__background-wrapper
Background image
background snippet; .advanced-cropper__background

Layer snippets receive typed arguments. Render their children, register a replacement boundary, and attach a replacement image or canvas as the export source.

Forward children and styling

Render a wrapper’s children exactly once. Keep descendants mounted while loading: hiding them visually is fine, but delaying their creation also delays stencil registration. Preserve supplied class and style, adding your own class with an array.

wrapper defaults to CropperWrapper; backgroundWrapper defaults to CropperBackgroundWrapper. The latter receives normalized movement, scaling and rotation options. Forward native events from custom gesture controls so modifier keys remain available.

Register a boundary

boundary receives registerBoundary, sizeAlgorithm and stretchAlgorithm. StretchableBoundary accepts these directly and handles registration and cleanup. A custom boundary must provide stretchTo(size): Promise<Size | null> and reset(): void. Return null while it cannot measure a usable area.

Registration returns cleanup owned by that registration. Replacing a boundary invalidates pending measurement; cleanup from the old boundary cannot remove the new one.

Register the actual export source

background receives attachSource. Apply the returned attachment to the actual displayed image or canvas. For asynchronous canvas drawing, pass a promise that resolves after pixels for the current image have been drawn. Replacing the source invalidates the old export immediately.

<canvas {@attach attachSource(drawingReady)}></canvas>

This is only the registration fragment: a complete background must also load and draw the image, apply transforms and forward styling. The image editor includes all those pieces and a matching preview.

With canvas export enabled, onReady waits for the current source and rendered state. A missing source prevents that readiness guarantee; canvas={false} permits a purely visual replacement. A preview background does not register an export source.