Skip to content

Svelte

Basic Example

svelte
<!-- AvatarUpload.svelte -->
<script lang="ts">
    import { onMount } from 'svelte'
    import { HqCropper, type HqCropperInstance } from 'hq-cropper'

    let avatar = $state('')
    let cropper: HqCropperInstance

    onMount(() => {
        cropper = HqCropper((base64) => {
            avatar = base64
        })
    })
</script>

<div>
    {#if avatar}
        <img src={avatar} alt="Avatar" />
    {/if}
    <button onclick={() => cropper.open()}>Upload Avatar</button>
</div>

With Configuration

svelte
<!-- ProfilePicture.svelte -->
<script lang="ts">
    import { onMount } from 'svelte'
    import { HqCropper, type HqCropperInstance, type ApplicationState } from 'hq-cropper'

    let image = $state('')
    let fileName = $state('')
    let error = $state<string | null>(null)
    let cropper: HqCropperInstance

    onMount(() => {
        cropper = HqCropper(
            (base64, blob, state: ApplicationState) => {
                image = base64
                fileName = state.fileName
                error = null
            },
            {
                outputSize: 256,
                type: 'jpeg',
                compression: 0.8,
                maxFileSize: 5 * 1024 * 1024,
            },
            undefined,
            (message) => {
                error = message
            }
        )
    })
</script>

<div class="profile-picture">
    {#if error}
        <div class="error">{error}</div>
    {/if}

    {#if image}
        <div class="preview">
            <img src={image} alt="Profile" />
            <p>{fileName}</p>
        </div>
    {:else}
        <div class="placeholder">No image selected</div>
    {/if}

    <button onclick={() => cropper.open()}>
        {image ? 'Change Image' : 'Upload Image'}
    </button>
</div>

Store-based Approach

typescript
// stores/cropper.svelte.ts
import {
    HqCropper,
    type HqCropperInstance,
    type ConfigurationOptions,
    type ApplicationState,
} from 'hq-cropper'

function createCropperStore(config?: Partial<ConfigurationOptions>) {
    let image = $state<string | null>(null)
    let blob = $state<Blob | null>(null)
    let state = $state<ApplicationState | null>(null)
    let error = $state<string | null>(null)
    let cropper: HqCropperInstance | null = null

    function init() {
        cropper = HqCropper(
            (base64, blobData, stateData) => {
                image = base64
                blob = blobData
                state = stateData
                error = null
            },
            config,
            undefined,
            (message) => {
                error = message
            }
        )
    }

    function open() {
        cropper?.open()
    }

    function clear() {
        image = null
        blob = null
        state = null
    }

    return {
        get image() {
            return image
        },
        get blob() {
            return blob
        },
        get state() {
            return state
        },
        get error() {
            return error
        },
        init,
        open,
        clear,
    }
}

export const cropperStore = createCropperStore({ outputSize: 512 })
svelte
<!-- App.svelte -->
<script lang="ts">
    import { onMount } from 'svelte'
    import { cropperStore } from './stores/cropper.svelte'

    onMount(() => {
        cropperStore.init()
    })
</script>

<div>
    {#if cropperStore.image}
        <img src={cropperStore.image} alt="" />
    {/if}
    <button onclick={() => cropperStore.open()}>Select Image</button>
    {#if cropperStore.image}
        <button onclick={() => cropperStore.clear()}>Clear</button>
    {/if}
</div>

With Form Submission

svelte
<!-- RegistrationForm.svelte -->
<script lang="ts">
    import { onMount } from 'svelte'
    import { HqCropper, type HqCropperInstance } from 'hq-cropper'

    let name = $state('')
    let email = $state('')
    let preview = $state('')
    let avatarBlob: Blob | null = null
    let cropper: HqCropperInstance

    onMount(() => {
        cropper = HqCropper(
            (base64, blob) => {
                preview = base64
                avatarBlob = blob
            },
            { outputSize: 256 }
        )
    })

    async function handleSubmit(e: SubmitEvent) {
        e.preventDefault()

        const formData = new FormData()
        formData.append('name', name)
        formData.append('email', email)

        if (avatarBlob) {
            formData.append('avatar', avatarBlob, 'avatar.jpg')
        }

        await fetch('/api/register', {
            method: 'POST',
            body: formData,
        })
    }
</script>

<form onsubmit={handleSubmit}>
    <div class="avatar-section">
        {#if preview}
            <img src={preview} alt="Avatar preview" />
        {/if}
        <button type="button" onclick={() => cropper.open()}>
            Upload Avatar
        </button>
    </div>

    <input bind:value={name} placeholder="Name" required />
    <input bind:value={email} type="email" placeholder="Email" required />

    <button type="submit">Register</button>
</form>

Reusable Component

svelte
<!-- ImageCropper.svelte -->
<script lang="ts">
    import { onMount } from 'svelte'
    import {
        HqCropper,
        type HqCropperInstance,
        type ConfigurationOptions,
        type ClassNames,
        type ApplicationState,
    } from 'hq-cropper'

    interface Props {
        config?: Partial<ConfigurationOptions>
        css?: Partial<ClassNames>
        onCrop?: (base64: string, blob: Blob | null, state: ApplicationState) => void
        onError?: (error: string) => void
    }

    let { config, css, onCrop, onError }: Props = $props()

    let cropper: HqCropperInstance

    onMount(() => {
        cropper = HqCropper(
            (base64, blob, state) => {
                onCrop?.(base64, blob, state)
            },
            config,
            css,
            onError
        )
    })

    export function open() {
        cropper.open()
    }
</script>

<button onclick={() => cropper.open()}>
    <slot>Select Image</slot>
</button>
svelte
<!-- Usage -->
<script lang="ts">
    import ImageCropper from './ImageCropper.svelte'

    let avatar = $state('')
    let cropperRef: ImageCropper

    function handleCrop(base64: string) {
        avatar = base64
    }
</script>

<div>
    {#if avatar}
        <img src={avatar} alt="Avatar" />
    {/if}

    <ImageCropper
        bind:this={cropperRef}
        config={{ outputSize: 256, type: 'jpeg' }}
        onCrop={handleCrop}
        onError={(e) => alert(e)}
    >
        Upload Avatar
    </ImageCropper>

    <!-- Or trigger programmatically -->
    <button onclick={() => cropperRef.open()}>
        Open Cropper
    </button>
</div>

Action-based Approach

typescript
// actions/cropper.ts
import {
    HqCropper,
    type HqCropperInstance,
    type ConfigurationOptions,
    type ApplicationState,
} from 'hq-cropper'

interface CropperParams {
    config?: Partial<ConfigurationOptions>
    onCrop: (base64: string, blob: Blob | null, state: ApplicationState) => void
    onError?: (error: string) => void
}

export function cropper(node: HTMLElement, params: CropperParams) {
    let instance: HqCropperInstance

    function init(params: CropperParams) {
        instance = HqCropper(
            params.onCrop,
            params.config,
            undefined,
            params.onError
        )
    }

    function handleClick() {
        instance.open()
    }

    init(params)
    node.addEventListener('click', handleClick)

    return {
        update(newParams: CropperParams) {
            init(newParams)
        },
        destroy() {
            node.removeEventListener('click', handleClick)
        },
    }
}
svelte
<!-- Usage with action -->
<script lang="ts">
    import { cropper } from './actions/cropper'

    let avatar = $state('')
</script>

<div>
    {#if avatar}
        <img src={avatar} alt="" />
    {/if}

    <button
        use:cropper={{
            config: { outputSize: 512 },
            onCrop: (base64) => avatar = base64,
            onError: (e) => console.error(e)
        }}
    >
        Upload
    </button>
</div>

With Tailwind CSS

svelte
<script lang="ts">
    import { onMount } from 'svelte'
    import { HqCropper, type HqCropperInstance } from 'hq-cropper'

    let image = $state('')
    let cropper: HqCropperInstance

    onMount(() => {
        cropper = HqCropper(
            (base64) => image = base64,
            { outputSize: 256 },
            {
                container: ['rounded-xl', 'shadow-2xl'],
                header: ['bg-gray-100', 'text-gray-800'],
                applyButton: [
                    'bg-blue-500',
                    'hover:bg-blue-600',
                    'text-white',
                    'px-4',
                    'py-2',
                    'rounded-lg',
                ],
                cancelButton: [
                    'bg-gray-200',
                    'hover:bg-gray-300',
                    'text-gray-700',
                    'px-4',
                    'py-2',
                    'rounded-lg',
                ],
            }
        )
    })
</script>

<div class="flex flex-col items-center gap-4">
    {#if image}
        <img src={image} alt="Cropped" class="w-32 h-32 rounded-full" />
    {/if}
    <button
        onclick={() => cropper.open()}
        class="bg-blue-500 text-white px-4 py-2 rounded"
    >
        Upload
    </button>
</div>

SvelteKit Integration

svelte
<!-- routes/profile/+page.svelte -->
<script lang="ts">
    import { onMount } from 'svelte'
    import { HqCropper, type HqCropperInstance } from 'hq-cropper'
    import { enhance } from '$app/forms'

    let preview = $state('')
    let avatarBlob: Blob | null = null
    let cropper: HqCropperInstance

    onMount(() => {
        cropper = HqCropper(
            (base64, blob) => {
                preview = base64
                avatarBlob = blob
            },
            { outputSize: 256 }
        )
    })

    function handleSubmit() {
        return async ({ formData }: { formData: FormData }) => {
            if (avatarBlob) {
                formData.append('avatar', avatarBlob, 'avatar.jpg')
            }
        }
    }
</script>

<form method="POST" use:enhance={handleSubmit}>
    <div class="avatar-section">
        {#if preview}
            <img src={preview} alt="Avatar" />
        {/if}
        <button type="button" onclick={() => cropper.open()}>
            Change Avatar
        </button>
    </div>

    <input name="name" placeholder="Name" required />

    <button type="submit">Update Profile</button>
</form>

Legacy Svelte 4 Syntax

svelte
<!-- Svelte 4 -->
<script lang="ts">
    import { onMount } from 'svelte'
    import { HqCropper, type HqCropperInstance } from 'hq-cropper'

    let avatar = ''
    let cropper: HqCropperInstance

    onMount(() => {
        cropper = HqCropper((base64) => {
            avatar = base64
        })
    })
</script>

<div>
    {#if avatar}
        <img src={avatar} alt="Avatar" />
    {/if}
    <button on:click={() => cropper.open()}>Upload Avatar</button>
</div>

Released under the MIT License.