Skip to content

Vanilla JavaScript

Basic HTML + JS

html
<!DOCTYPE html>
<html>
    <head>
        <title>HQ-Cropper Demo</title>
    </head>
    <body>
        <img id="preview" style="display: none; max-width: 200px;" />
        <button id="upload">Upload Image</button>

        <script type="module">
            import { HqCropper } from 'https://esm.sh/hq-cropper@4'

            const preview = document.getElementById('preview')
            const button = document.getElementById('upload')

            const cropper = HqCropper((base64) => {
                preview.src = base64
                preview.style.display = 'block'
            })

            button.addEventListener('click', () => cropper.open())
        </script>
    </body>
</html>

With Bundler

typescript
// main.ts
import { HqCropper } from 'hq-cropper'

const preview = document.getElementById('preview') as HTMLImageElement
const button = document.getElementById('upload') as HTMLButtonElement

const cropper = HqCropper(
    (base64, blob, state) => {
        preview.src = base64
        preview.style.display = 'block'
        console.log(`Cropped ${state.fileName}: ${blob?.size} bytes`)
    },
    {
        outputSize: 512,
        compression: 0.9,
    }
)

button.addEventListener('click', () => cropper.open())

File Upload

typescript
import { HqCropper } from 'hq-cropper'

const cropper = HqCropper(async (base64, blob, state) => {
    if (!blob) return

    // Show preview
    document.getElementById('preview').src = base64

    // Upload
    const formData = new FormData()
    formData.append('image', blob, state.fileName)

    try {
        const response = await fetch('/api/upload', {
            method: 'POST',
            body: formData,
        })

        const { url } = await response.json()
        document.getElementById('result').textContent = `Uploaded: ${url}`
    } catch (error) {
        document.getElementById('error').textContent = error.message
    }
})

document.getElementById('upload-btn').addEventListener('click', () => {
    cropper.open()
})

Avatar Component (Class-based)

typescript
import {
    HqCropper,
    type HqCropperInstance,
    type ConfigurationOptions,
} from 'hq-cropper'

class AvatarUploader {
    private cropper: HqCropperInstance
    private previewEl: HTMLImageElement
    private currentBlob: Blob | null = null

    constructor(
        buttonSelector: string,
        previewSelector: string,
        config?: Partial<ConfigurationOptions>
    ) {
        this.previewEl = document.querySelector(previewSelector)!

        this.cropper = HqCropper(
            (base64, blob) => {
                this.previewEl.src = base64
                this.previewEl.style.display = 'block'
                this.currentBlob = blob
            },
            { outputSize: 256, ...config }
        )

        document
            .querySelector(buttonSelector)!
            .addEventListener('click', () => this.open())
    }

    open() {
        this.cropper.open()
    }

    getBlob(): Blob | null {
        return this.currentBlob
    }

    clear() {
        this.previewEl.src = ''
        this.previewEl.style.display = 'none'
        this.currentBlob = null
    }
}

// Usage
const avatarUploader = new AvatarUploader('#upload-btn', '#preview', {
    type: 'jpeg',
    compression: 0.8,
})

// Get blob for form submission
document.querySelector('form')!.addEventListener('submit', (e) => {
    e.preventDefault()
    const blob = avatarUploader.getBlob()
    if (blob) {
        // Submit blob...
    }
})

Multiple Instances

html
<div class="user-1">
    <img id="avatar-1" />
    <button id="btn-1">User 1 Avatar</button>
</div>

<div class="user-2">
    <img id="avatar-2" />
    <button id="btn-2">User 2 Avatar</button>
</div>

<script type="module">
    import { HqCropper } from 'hq-cropper'

    const createCropper = (btnId, imgId) => {
        const img = document.getElementById(imgId)
        const btn = document.getElementById(btnId)

        const cropper = HqCropper(
            (base64) => {
                img.src = base64
            },
            { outputSize: 256 }
        )

        btn.addEventListener('click', () => cropper.open())
    }

    createCropper('btn-1', 'avatar-1')
    createCropper('btn-2', 'avatar-2')
</script>

Custom Styling

html
<style>
    .dark-modal {
        background: #1a1a1a;
        color: #fff;
        border-radius: 12px;
    }
    .dark-header {
        background: #2a2a2a;
        border-bottom: 1px solid #333;
    }
    .btn-primary {
        background: #3b82f6;
        color: white;
        border: none;
        padding: 8px 16px;
        border-radius: 6px;
        cursor: pointer;
    }
    .btn-secondary {
        background: #4a4a4a;
        color: #ccc;
        border: none;
        padding: 8px 16px;
        border-radius: 6px;
        cursor: pointer;
    }
</style>

<script type="module">
    import { HqCropper } from 'hq-cropper'

    const cropper = HqCropper(
        (base64) => console.log(base64),
        {},
        {
            container: ['dark-modal'],
            header: ['dark-header'],
            applyButton: ['btn-primary'],
            cancelButton: ['btn-secondary'],
        }
    )

    document.getElementById('open').addEventListener('click', () => {
        cropper.open()
    })
</script>

Released under the MIT License.