Here is a complete, single-file solution for a modern and responsive Image Compressor. It uses HTML5, Tailwind CSS (via CDN) for styling, and vanilla JavaScript to handle the compression logic completely within the browser using the <canvas> element.
You can save the code below as an .html file (e.g., index.html) and open it directly in any modern web browser.
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modern Image Compressor</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Custom drag-active state styling */
.drag-active {
border-color: #3b82f6 !important;
background-color: #eff6ff !important;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen text-gray-800 font-sans p-4 md:p-8 flex items-center justify-center">
<div class="max-w-4xl w-full bg-white rounded-2xl shadow-lg overflow-hidden flex flex-col transition-all duration-300">
<div class="bg-blue-600 p-6 text-center">
<h1 class="text-2xl md:text-3xl font-bold text-white mb-2">Image Compressor</h1>
<p class="text-blue-100 text-sm md:text-base">Compress your images locally in the browser. Fast, secure, and private.</p>
</div>
<div class="p-6 md:p-8">
<div id="upload-area" class="border-2 border-dashed border-gray-300 rounded-xl p-10 text-center cursor-pointer hover:bg-gray-50 hover:border-blue-500 transition-colors duration-200">
<input type="file" id="file-input" class="hidden" accept="image/jpeg, image/png, image/webp" />
<svg class="mx-auto h-12 w-12 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
</svg>
<p class="text-lg font-medium text-gray-700">Click to upload or drag and drop</p>
<p class="text-sm text-gray-500 mt-2">Supports JPG, PNG, and WebP</p>
</div>
<div id="workspace" class="hidden mt-8 flex flex-col gap-8">
<div class="bg-gray-50 p-6 rounded-xl border border-gray-200 grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Compression Quality: <span id="quality-value" class="font-bold text-blue-600">80%</span>
</label>
<input type="range" id="quality-slider" min="0.1" max="1" step="0.05" value="0.8" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Output Format</label>
<select id="format-select" class="block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<option value="image/jpeg">JPEG</option>
<option value="image/webp">WebP</option>
<option value="image/png">PNG (Lossless)</option>
</select>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="flex flex-col border border-gray-200 rounded-xl overflow-hidden bg-gray-50">
<div class="p-3 bg-gray-100 border-b border-gray-200 flex justify-between items-center text-sm">
<span class="font-semibold text-gray-700">Original</span>
<span id="original-size" class="text-gray-500">0 KB</span>
</div>
<div class="p-4 flex items-center justify-center bg-checkered min-h-[250px]">
<img id="original-img" class="max-w-full max-h-[300px] object-contain rounded shadow-sm" alt="Original Preview">
</div>
</div>
<div class="flex flex-col border border-gray-200 rounded-xl overflow-hidden bg-gray-50">
<div class="p-3 bg-blue-50 border-b border-blue-100 flex justify-between items-center text-sm">
<span class="font-semibold text-blue-700">Compressed</span>
<span id="compressed-size" class="text-blue-600 font-medium">0 KB</span>
</div>
<div class="p-4 flex items-center justify-center min-h-[250px]">
<img id="compressed-img" class="max-w-full max-h-[300px] object-contain rounded shadow-sm" alt="Compressed Preview">
</div>
</div>
</div>
<div class="flex flex-col sm:flex-row justify-end gap-4 mt-4">
<button id="reset-btn" class="px-6 py-2.5 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
Upload Another Image
</button>
<a id="download-btn" class="px-6 py-2.5 shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors text-center cursor-pointer flex items-center justify-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
Download Compressed
</a>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// DOM Elements
const uploadArea = document.getElementById('upload-area');
const fileInput = document.getElementById('file-input');
const workspace = document.getElementById('workspace');
const qualitySlider = document.getElementById('quality-slider');
const qualityValue = document.getElementById('quality-value');
const formatSelect = document.getElementById('format-select');
const originalImg = document.getElementById('original-img');
const compressedImg = document.getElementById('compressed-img');
const originalSizeTxt = document.getElementById('original-size');
const compressedSizeTxt = document.getElementById('compressed-size');
const resetBtn = document.getElementById('reset-btn');
const downloadBtn = document.getElementById('download-btn');
let currentFile = null;
let currentImageObject = new Image();
// Utility to format file sizes
const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
// Event Listeners for Upload
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('drag-active');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('drag-active');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('drag-active');
if (e.dataTransfer.files.length) {
handleFile(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length) {
handleFile(e.target.files[0]);
}
});
// Process the uploaded file
function handleFile(file) {
if (!file.type.startsWith('image/')) {
alert('Please upload an image file (JPEG, PNG, WebP).');
return;
}
currentFile = file;
originalSizeTxt.textContent = formatBytes(file.size);
const reader = new FileReader();
reader.onload = (e) => {
currentImageObject.src = e.target.result;
currentImageObject.onload = () => {
originalImg.src = currentImageObject.src;
uploadArea.classList.add('hidden');
workspace.classList.remove('hidden');
compressImage(); // Initial compression run
};
};
reader.readAsDataURL(file);
}
// Core Compression Logic
function compressImage() {
if (!currentImageObject.src) return;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Keep original dimensions
canvas.width = currentImageObject.width;
canvas.height = currentImageObject.height;
// Draw image on canvas
ctx.drawImage(currentImageObject, 0, 0, canvas.width, canvas.height);
// Get settings
const quality = parseFloat(qualitySlider.value);
const format = formatSelect.value;
// Compress
const compressedDataUrl = canvas.toDataURL(format, quality);
compressedImg.src = compressedDataUrl;
// Calculate rough size from base64 string
const base64Length = compressedDataUrl.length - (compressedDataUrl.indexOf(',') + 1);
const sizeInBytes = Math.ceil((base64Length * 3) / 4);
compressedSizeTxt.textContent = formatBytes(sizeInBytes);
// Setup Download
const ext = format.split('/')[1];
const originalName = currentFile.name.split('.')[0];
downloadBtn.href = compressedDataUrl;
downloadBtn.download = `${originalName}-compressed.${ext}`;
}
// Controls Listeners
qualitySlider.addEventListener('input', (e) => {
qualityValue.textContent = `${Math.round(e.target.value * 100)}%`;
compressImage();
});
formatSelect.addEventListener('change', () => {
// PNG compression via canvas is lossless, slider won't affect it much.
if(formatSelect.value === 'image/png') {
qualitySlider.disabled = true;
qualitySlider.classList.add('opacity-50');
qualityValue.textContent = 'N/A (Lossless)';
} else {
qualitySlider.disabled = false;
qualitySlider.classList.remove('opacity-50');
qualityValue.textContent = `${Math.round(qualitySlider.value * 100)}%`;
}
compressImage();
});
// Reset Logic
resetBtn.addEventListener('click', () => {
currentFile = null;
fileInput.value = '';
uploadArea.classList.remove('hidden');
workspace.classList.add('hidden');
});
});
</script>
</body>
</html>
