Tools
hq-cropper: Zero-Dependency Image Cropper for JS
2025-12-13
0 views
admin
The Problem with Large Images ## How hq-cropper Solves This ## How It Works ## Practical Examples ## Avatar Upload (Balance Quality & Size) ## Thumbnail Generation (Smallest Possible) ## High-Quality Crop (Preserve Details) ## Real-World Comparison ## Additional Output Controls ## Why Another Image Cropper? ## Features ## Quick Start ## React Example ## All Configuration Options ## What's New in v3.2.0 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. 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: 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. hq-cropper uses a logarithmic scaling algorithm controlled by the quality parameter. Here's the key insight: The quality parameter (default: 1.01) controls this behavior. It's the logarithm base used to calculate output dimensions from the crop selection size. For profile pictures where you want decent quality but reasonable file sizes: Result: A 500px crop selection produces ~180px output. A 200px selection produces ~150px output. Small selections stay sharp, large selections get reasonably compressed. When file size matters most (e.g., gallery thumbnails): Result: Aggressive downscaling. A 500px selection → ~130px output. Perfect for thumbnails where you need tiny files. When quality is paramount (e.g., portfolio images): Result: Nearly 1:1 output. A 500px selection → ~490px output. Maximum quality, larger files. Notice how smaller source selections maintain more relative size — this preserves quality when users are already working with smaller images. Beyond quality, you have fine-grained control: Compression is standard JPEG quality (0.0 - 1.0). Combined with quality, you get precise control: Most existing solutions are either: I wanted something that: If you find this useful, give it a ⭐ on GitHub! Questions? Drop a comment below. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
outputSize = log(cropSelectionSize) / log(quality) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
outputSize = log(cropSelectionSize) / log(quality) CODE_BLOCK:
outputSize = log(cropSelectionSize) / log(quality) CODE_BLOCK:
const cropper = HqCropper(onSubmit, { quality: 1.5, compression: 0.85, type: 'jpeg',
}) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const cropper = HqCropper(onSubmit, { quality: 1.5, compression: 0.85, type: 'jpeg',
}) CODE_BLOCK:
const cropper = HqCropper(onSubmit, { quality: 1.5, compression: 0.85, type: 'jpeg',
}) CODE_BLOCK:
const cropper = HqCropper(onSubmit, { quality: 2.0, compression: 0.7, type: 'jpeg',
}) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const cropper = HqCropper(onSubmit, { quality: 2.0, compression: 0.7, type: 'jpeg',
}) CODE_BLOCK:
const cropper = HqCropper(onSubmit, { quality: 2.0, compression: 0.7, type: 'jpeg',
}) CODE_BLOCK:
const cropper = HqCropper(onSubmit, { quality: 1.01, compression: 1, type: 'png',
}) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const cropper = HqCropper(onSubmit, { quality: 1.01, compression: 1, type: 'png',
}) CODE_BLOCK:
const cropper = HqCropper(onSubmit, { quality: 1.01, compression: 1, type: 'png',
}) CODE_BLOCK:
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 CODE_BLOCK:
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'
}) CODE_BLOCK:
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'
}) COMMAND_BLOCK:
npm install hq-cropper Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
npm install hq-cropper COMMAND_BLOCK:
npm install hq-cropper COMMAND_BLOCK:
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 COMMAND_BLOCK:
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()
}) COMMAND_BLOCK:
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()
}) COMMAND_BLOCK:
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 COMMAND_BLOCK:
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> )
} COMMAND_BLOCK:
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> )
} COMMAND_BLOCK:
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 COMMAND_BLOCK:
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)
) COMMAND_BLOCK:
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)
) - 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 - Small source images → minimal or no downscaling (preserves quality)
- Large source images → proportional downscaling (reduces file size) - quality: 1.01 → Large output (almost 1:1 with selection)
- quality: 1.5 → Medium output (good balance)
- quality: 2.0 → Small output (aggressive compression) - 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 - 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 - Works everywhere (vanilla JS, React, Vue, Angular)
- Has zero dependencies
- Focuses on square crops (perfect for avatars)
- Intelligently handles any source image size - 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 - Fixed memory leaks (proper cleanup on modal close)
- Fixed race conditions in canvas operations
- Fixed resize handles in all corners - onError callback for graceful error handling
- maxFileSize and allowedTypes for file validation
- minPortalSize to prevent tiny crop areas - DOM element caching
- requestAnimationFrame throttling for smooth dragging - Live Demo & Storybook
- GitHub Repository
- npm Package
how-totutorialguidedev.toaigitgithub