DEV Community

Cover image for hq-cropper: Zero-Dependency Image Cropper for JS
Iakov Salikov
Iakov Salikov

Posted on

hq-cropper: Zero-Dependency Image Cropper for JS

hq-cropper: Zero-Dependency Image Cropper for JS

Have you ever needed a simple, lightweight image cropper for profile pictures or avatars? I've been working on hq-cropper — a zero-dependency TypeScript library that does exactly that.

The Problem with Large Images

Here's a common scenario: your user uploads a 4000×3000 pixel photo from their smartphone, but you only need a 200×200 avatar. Most croppers handle this poorly:

  • Naive approach: Crop at full resolution, then resize → wastes memory, slow on mobile
  • Simple resize: Downscale first, then crop → loses too much quality
  • Fixed output size: Always outputs the same dimensions → no flexibility

The challenge is finding the right balance: you want small output files, but you don't want to destroy quality when the source image is already small.

How hq-cropper Solves This

hq-cropper uses a logarithmic scaling algorithm controlled by the quality parameter. Here's the key insight:

  • Small source images → minimal or no downscaling (preserves quality)
  • Large source images → proportional downscaling (reduces file size)

The quality parameter (default: 1.01) controls this behavior. It's the logarithm base used to calculate output dimensions from the crop selection size.

How It Works

outputSize = log(cropSelectionSize) / log(quality)
Enter fullscreen mode Exit fullscreen mode
  • quality: 1.01 → Large output (almost 1:1 with selection)
  • quality: 1.5 → Medium output (good balance)
  • quality: 2.0 → Small output (aggressive compression)

Practical Examples

Avatar Upload (Balance Quality & Size)

For profile pictures where you want decent quality but reasonable file sizes:

const cropper = HqCropper(onSubmit, {
    quality: 1.5,
    compression: 0.85,
    type: 'jpeg',
})
Enter fullscreen mode Exit fullscreen mode

Result: A 500px crop selection produces ~180px output. A 200px selection produces ~150px output. Small selections stay sharp, large selections get reasonably compressed.

Thumbnail Generation (Smallest Possible)

When file size matters most (e.g., gallery thumbnails):

const cropper = HqCropper(onSubmit, {
    quality: 2.0,
    compression: 0.7,
    type: 'jpeg',
})
Enter fullscreen mode Exit fullscreen mode

Result: Aggressive downscaling. A 500px selection → ~130px output. Perfect for thumbnails where you need tiny files.

High-Quality Crop (Preserve Details)

When quality is paramount (e.g., portfolio images):

const cropper = HqCropper(onSubmit, {
    quality: 1.01,
    compression: 1,
    type: 'png',
})
Enter fullscreen mode Exit fullscreen mode

Result: Nearly 1:1 output. A 500px selection → ~490px output. Maximum quality, larger files.

Real-World Comparison

Source Image Crop Selection quality: 1.01 quality: 1.5 quality: 2.0
4000×3000 800px ~780px ~210px ~130px
1200×800 400px ~390px ~170px ~120px
400×400 200px ~195px ~150px ~110px

Notice how smaller source selections maintain more relative size — this preserves quality when users are already working with smaller images.

Additional Output Controls

Beyond quality, you have fine-grained control:

const cropper = HqCropper(onSubmit, {
    // Logarithmic scaling factor
    quality: 1.5,

    // JPEG compression (0-1, where 1 is best)
    compression: 0.85,

    // Output format
    type: 'jpeg', // or 'png'
})
Enter fullscreen mode Exit fullscreen mode

Compression is standard JPEG quality (0.0 - 1.0). Combined with quality, you get precise control:

  • quality: 1.5 + compression: 0.85 → Balanced (recommended for avatars)
  • quality: 2.0 + compression: 0.7 → Smallest files
  • quality: 1.01 + compression: 1 + type: 'png' → Maximum quality

Why Another Image Cropper?

Most existing solutions are either:

  • Tied to a specific framework (React, Vue, etc.)
  • Bloated with dependencies
  • Overcomplicated for simple use cases
  • Don't handle the large-to-small image problem well

I wanted something that:

  • Works everywhere (vanilla JS, React, Vue, Angular)
  • Has zero dependencies
  • Focuses on square crops (perfect for avatars)
  • Intelligently handles any source image size

Features

  • Zero dependencies — pure TypeScript, ~22KB minified
  • Framework agnostic — works with any stack
  • Smart scaling — logarithmic algorithm for optimal output
  • Drag & resize — intuitive UI with corner handles
  • File validation — built-in type and size checks
  • Error handling — callback-based error reporting
  • Fully typed — complete TypeScript support

Quick Start

npm install hq-cropper
Enter fullscreen mode Exit fullscreen mode
import { HqCropper } from 'hq-cropper'

const cropper = HqCropper((base64, blob, state) => {
    document.querySelector('img').src = base64
    console.log(`Cropped ${state.fileName}: ${blob?.size} bytes`)
})

document.querySelector('button').addEventListener('click', () => {
    cropper.open()
})
Enter fullscreen mode Exit fullscreen mode

React Example

import { useRef, useState } from 'react'
import { HqCropper } from 'hq-cropper'

function AvatarUpload() {
    const [avatar, setAvatar] = useState('')

    const cropperRef = useRef(
        HqCropper(
            (base64) => setAvatar(base64),
            {
                portalSize: 200,
                quality: 1.5,
                compression: 0.85,
            },
            undefined,
            (error) => console.error(error)
        )
    )

    return (
        <div>
            {avatar && <img src={avatar} alt="Avatar" />}
            <button onClick={() => cropperRef.current.open()}>
                Upload Avatar
            </button>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

All Configuration Options

const cropper = HqCropper(
    onSubmit,
    {
        // Portal (crop area) settings
        portalSize: 150,
        minPortalSize: 50,
        portalPosition: 'center',

        // Output settings
        type: 'jpeg',
        quality: 1.5,
        compression: 0.85,

        // Validation
        maxFileSize: 5 * 1024 * 1024,
        allowedTypes: ['image/jpeg', 'image/png'],

        // UI labels
        applyButtonLabel: 'Save',
        cancelButtonLabel: 'Cancel',
    },
    undefined,
    (error) => alert(error)
)
Enter fullscreen mode Exit fullscreen mode

What's New in v3.2.0

Bug Fixes:

  • Fixed memory leaks (proper cleanup on modal close)
  • Fixed race conditions in canvas operations
  • Fixed resize handles in all corners

New Features:

  • onError callback for graceful error handling
  • maxFileSize and allowedTypes for file validation
  • minPortalSize to prevent tiny crop areas

Performance:

  • DOM element caching
  • requestAnimationFrame throttling for smooth dragging

Links


If you find this useful, give it a ⭐ on GitHub! Questions? Drop a comment below.

Top comments (0)