About this course
<p>High-resolution images look great, but they can significantly slow down page load times and consume massive amounts of storage.</p>
<p>While backend compression tools are common, building a client-side image compressor offers a massive advantage: privacy. When you process images directly in the browser, no user data ever touches a server.</p>
<p>In this tutorial, you'll learn how to build a fully functional, browser-based bulk image compressor. You'll use the HTML5 Canvas API to reduce image file sizes and integrate the JSZip library to package multiple compressed files into a single, convenient ZIP download.</p>
<p>To make this project highly practical, you'll structure the code as an embeddable widget. By omitting standard HTML boilerplate tags, you can easily drop this snippet directly into a WordPress Custom HTML block or any other CMS without causing layout conflicts.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you should have a basic understanding of:</p>
<ul>
<li><p><strong>HTML & CSS:</strong> Structuring a UI and creating interactive hover/drag states.</p>
</li>
<li><p><strong>JavaScript Promises:</strong> Handling asynchronous operations like file reading and ZIP generation.</p>
</li>
<li><p><strong>The Canvas API:</strong> Understanding how browsers can draw and manipulate image data natively.</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-step-1-build-the-html-structure">Step 1: Build the HTML Structure</a></p>
<ul>
<li><a href="#heading-understanding-the-html">Understanding the HTML</a></li>
</ul>
</li>
<li><p><a href="#heading-step-2-style-the-interface-with-css">Step 2: Style the Interface with CSS</a></p>
<ul>
<li><a href="#heading-understanding-the-css">Understanding the CSS</a></li>
</ul>
</li>
<li><p><a href="#heading-step-3-add-the-javascript-logic">Step 3: Add the JavaScript Logic</a></p>
<ul>
<li><a href="#heading-understanding-the-javascript">Understanding the JavaScript</a></li>
</ul>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-step-1-build-the-html-structure">Step 1: Build the HTML Structure</h2>
<p>First, you need to create the visual interface. You'll build a drag-and-drop zone, an invisible file input, and a hidden action panel that appears once the user selects their images.</p>
<p>Add the following code to your file. Notice that you're including the JSZip CDN link right at the top so your script can access it later.</p>
<pre><code class="language-HTML"><!-- JSZip Library for Bulk Downloading -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<div class="ic-main-wrapper">
<div class="ic-title">Image Compressor</div>
<!-- The Drag and Drop Zone -->
<div class="ic-dropzone-area" id="icDropzone">
<input type="file" id="icFileInput" multiple accept="image/jpeg, image/png, image/webp, image/gif, image/bmp, image/tiff, image/avif" style="display: none;">
<div class="ic-icon-box">
<svg viewBox="0 0 24 24">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zm-5-7l-3 3.72L9 13l-3 4h14l-4-5z"/>
</svg>
</div>
<div class="ic-primary-text">
Drop images here or <span class="ic-browse-link" id="icBrowseBtn">click to browse</span>
</div>
<div class="ic-secondary-text">
Multiple images supported — bulk compress & download as ZIP
</div>
<div class="ic-format-tags">
<span>JPEG</span><span>PNG</span><span>WebP</span><span>GIF</span><span>BMP</span><span>TIFF</span><span>AVIF</span>
</div>
</div>
<!-- Action Panel (Hidden by default) -->
<div class="ic-action-panel" id="icActionPanel">
<div id="icStatusText" class="ic-status-msg">0 images selected</div>
<button class="ic-btn-primary" id="icCompressBtn">Compress & Download ZIP</button>
</div>
</div>
</code></pre>
<h3 id="heading-understanding-the-html">Understanding the HTML:</h3>
<ul>
<li><p><code>accept="..."</code> <strong>attribute:</strong> This restricts the hidden <code><input type="file"></code> to only accept image formats, preventing users from accidentally uploading PDFs or text documents.</p>
</li>
<li><p><strong>Embeddable Structure:</strong> Because this markup relies on a single <code>.ic-main-wrapper</code> container rather than full <code><html></code> and <code><body></code> tags, you can safely embed it into existing web pages without breaking the parent theme.</p>
</li>
</ul>
<h2 id="heading-step-2-style-the-interface-with-css">Step 2: Style the Interface with CSS</h2>
<p>Next, you'll apply styling to make the tool look professional and responsive. You'll use a clean, modern aesthetic with a specific brand accent color (<code>#1A73E8</code>) to highlight interactive elements.</p>
<p>Add this <code><style></code> block right above your HTML:</p>
<pre><code class="language-CSS"><style>
.ic-main-wrapper {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 850px;
margin: 20px auto;
background: #ffffff;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 20px rgba(0,0,0,0.04);
}
.ic-title {
font-size: 2.2rem;
font-weight: 700;
color: #0f172a;
text-align: center;
margin-bottom: 30px;
}
.ic-dropzone-area {
border: 1.5px dashed #cbd5e1;
border-radius: 12px;
padding: 60px 20px;
text-align: center;
background-color: #f8fafc;
transition: all 0.3s ease;
}
/* Active Drag State */
.ic-dropzone-area.dragover {
border-color: #1A73E8;
background-color: #f1f5f9;
}
.ic-icon-box {
width: 64px;
height: 64px;
background-color: #e8f0fe;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24px;
color: #1A73E8;
}
.ic-icon-box svg {
width: 28px;
height: 28px;
fill: currentColor;
}
.ic-primary-text {
font-size: 1.15rem;
color: #0f172a;
font-weight: 500;
margin-bottom: 12px;
}
.ic-browse-link {
color: #1A73E8;
cursor: pointer;
text-decoration: none;
font-weight: 500;
}
.ic-browse-link:hover {
text-decoration: underline;
}
.ic-secondary-text {
font-size: 0.95rem;
color: #64748b;
margin-bottom: 24px;
}
.ic-format-tags {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
}
.ic-format-tags span {
border: 1px solid #e2e8f0;
background: #ffffff;
color: #64748b;
font-size: 0.8rem;
padding: 6px 18px;
border-radius: 20px;
}
.ic-action-panel {
margin-top: 25px;
display: none;
text-align: center;
background: #f8fafc;
padding: 20px;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.ic-btn-primary {
background: #1A73E8;
color: #ffffff;
border: none;
padding: 12px 28px;
border-radius: 6px;
font-size: 1rem;
cursor: pointer;
font-weight: 500;
transition: background 0.3s;
}
.ic-btn-primary:hover {
background: #1557b0;
}
.ic-btn-primary:disabled {
background: #94a3b8;
cursor: not-allowed;
}
.ic-status-msg {
margin-bottom: 15px;
font-size: 0.95rem;
color: #334155;
font-weight: 500;
}
</style>
</code></pre>
<h3 id="heading-understanding-the-css">Understanding the CSS:</h3>
<ul>
<li><p><strong>The</strong> <code>.dragover</code> <strong>Class:</strong> This class alters the border and background color of the dropzone. You'll use JavaScript to apply this class dynamically when a user hovers a file over the area, providing crucial visual feedback.</p>
</li>
<li><p><strong>Unique Prefixes:</strong> Notice how every class starts with <code>ic-</code> (Image Compressor). This acts as a CSS namespace, ensuring your tool's styles won't accidentally override or be overridden by other styles on your website.</p>
</li>
</ul>
<p>At this stage, your user interface is fully structured and styled. It will look like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/699c7b22cf5def0f6aaf982b/2e75b26b-e1ae-443b-a78f-6f9833a4774a.png" alt="2e75b26b-e1ae-443b-a78f-6f9833a4774a" style="display: block;" width="600" height="400" loading="lazy">
<h2 id="heading-step-3-add-the-javascript-logic">Step 3: Add the JavaScript Logic</h2>
<p>Now comes the engine of the tool. The JavaScript will handle file selection, convert the images using the Canvas API to reduce their size, and package them into a ZIP file.</p>
<p>Add this <code><script></code> block below your HTML:</p>
<pre><code class="language-javascript"><script>
document.addEventListener('DOMContentLoaded', () => {
const dropzone = document.getElementById('icDropzone');
const fileInput = document.getElementById('icFileInput');
const browseBtn = document.getElementById('icBrowseBtn');
const actionPanel = document.getElementById('icActionPanel');
const statusText = document.getElementById('icStatusText');
const compressBtn = document.getElementById('icCompressBtn');
let filesArray = [];
// 1. Handle File Input and Drag & Drop
browseBtn.addEventListener('click', (e) => {
e.stopPropagation();
fileInput.click();
});
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('dragover');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('dragover');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('dragover');
if (e.dataTransfer.files.length) {
processSelectedFiles(e.dataTransfer.files);
}
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length) {
processSelectedFiles(fileInput.files);
}
});
// 2. Validate and Display the Action Panel
function processSelectedFiles(files) {
filesArray = Array.from(files).filter(file => file.type.startsWith('image/'));
if (filesArray.length > 0) {
actionPanel.style.display = 'block';
statusText.innerText = `${filesArray.length} image(s) selected. Ready to compress.`;
}
}
// 3. The Core Compression Engine
function compressImage(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
// Draw the image onto the canvas
ctx.drawImage(img, 0, 0);
// Force compatibility: Convert unusual formats to JPEG or WebP
let mimeType = file.type;
if (mimeType !== 'image/jpeg' && mimeType !== 'image/webp') {
mimeType = 'image/jpeg';
}
// Compress using the canvas.toBlob method at 70% quality (0.7)
canvas.toBlob((blob) => {
let finalName = file.name;
// Ensure the file extension matches the new mimeType
if (mimeType === 'image/jpeg' && !finalName.match(/\.(jpg|jpeg)$/i)) {
finalName = finalName.substring(0, finalName.lastIndexOf('.')) + '.jpg';
}
resolve({ name: finalName, blob: blob });
}, mimeType, 0.7);
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
});
}
// 4. Handle the Bulk ZIP Process
compressBtn.addEventListener('click', async () => {
if (filesArray.length === 0) return;
compressBtn.disabled = true;
try {
const zip = new JSZip();
const folder = zip.folder("compressed_images");
// Loop through each file and await its compression
for (let i = 0; i < filesArray.length; i++) {
statusText.innerText = `Compressing ${i + 1} of ${filesArray.length}...`;
const compressedData = await compressImage(filesArray[i]);
folder.file(compressedData.name, compressedData.blob);
}
statusText.innerText = 'Creating ZIP file...';
// Generate and trigger the ZIP download
const zipContent = await zip.generateAsync({ type: "blob" });
const downloadLink = document.createElement('a');
downloadLink.href = URL.createObjectURL(zipContent);
downloadLink.download = "compressed_images.zip";
downloadLink.click();
statusText.innerText = 'Download successful!';
} catch (error) {
statusText.innerText = 'An error occurred during compression.';
console.error(error);
} finally {
compressBtn.disabled = false;
}
});
});
</script>
</code></pre>
<h3 id="heading-understanding-the-javascript">Understanding the JavaScript:</h3>
<ul>
<li><p><strong>Drag and drop events:</strong> The <code>dragover</code>, <code>dragleave</code>, and <code>drop</code> event listeners work together to capture files dragged from a user's desktop directly into the browser window.</p>
</li>
<li><p><strong>The canvas trick:</strong> Browsers can't magically compress files on their own. Instead, the <code>compressImage</code> function reads the image, draws it onto an invisible HTML5 <code><canvas></code>, and then uses the <code>canvas.toBlob()</code> method to export a new, optimized version of that image. The <code>0.7</code> argument sets the compression quality to 70%.</p>
</li>
<li><p><strong>Extension handling:</strong> Because the canvas exports images as either JPEG or WebP, the script actively checks and renames the file extensions (for example, changing a <code>.png</code> filename to <code>.jpg</code>) before packaging them.</p>
</li>
<li><p><strong>Asynchronous zipping:</strong> A <code>for</code> loop uses <code>await</code> to compress each image sequentially. Once complete, JSZip bundles the blobs into a folder and triggers a programmatic click event on a temporary anchor <code><a></code> tag to download the final <code>.zip</code> file.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've successfully built a fast, client-side bulk image compressor.</p>
<p>By leveraging the Canvas API alongside JSZip, you created a highly practical utility that saves users bandwidth and storage without compromising their privacy. Because the entire codebase is self-contained without HTML boilerplate, it's ready to be deployed instantly on almost any modern web platform.</p>
<p>If you want to see this codebase deployed in a live, production environment, you can test out the functionality over at this Live <a href="https://99tools.net/image-compressor/">Image Compressor Tool</a>. Happy coding!</p>