How to Build a Bulk Image Compressor Tool with HTML, CSS, and

How to Build a Bulk Image Compressor Tool with HTML, CSS, and JavaScript


High-resolution images look great, but they can significantly slow down page load times and consume massive amounts of storage. In an era where Core Web Vitals and sustainable web practices drive SEO rankings and user retention, image optimization has become non-negotiable for modern web development.


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. This approach aligns perfectly with 2026's privacy-first web standards and eliminates server costs entirely.


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.


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.


Prerequisites


To follow along, you should have a basic understanding of:


  • HTML & CSS: Structuring a UI and creating interactive hover/drag states.
  • JavaScript Promises: Handling asynchronous operations like file reading and ZIP generation.
  • The Canvas API: Understanding how browsers can draw and manipulate image data natively.

Table of Contents



Step 1: Build the HTML Structure


The first step is to create the user interface. This includes a drag-and-drop zone, a file input fallback, a quality slider, and action buttons for compressing and downloading images.


<div class="bulk-compressor-widget">
  <div class="drop-zone" id="dropZone">
    <p>Drag & drop images here or <span class="browse-link">browse</span></p>
    <input type="file" id="fileInput" accept="image/*" multiple hidden>
  </div>

  <div class="controls">
    <label for="qualitySlider">Compression Quality: <span id="qualityValue">75%</span></label>
    <input type="range" id="qualitySlider" min="10" max="100" value="75">

    <button id="compressBtn" disabled>Compress Images</button>
    <button id="downloadBtn" disabled>Download ZIP</button>
  </div>

  <div class="preview-grid" id="previewGrid"></div>
</div>

Understanding the HTML


  • .bulk-compressor-widget acts as a scoped container, ensuring your styles don't leak into the host page.
  • #dropZone handles drag-and-drop interactions, while #fileInput provides a fallback for users who prefer browsing.
  • #qualitySlider lets users control the balance between file size and visual fidelity.
  • #previewGrid will display thumbnails and compression results dynamically.

Step 2: Style the Interface with CSS


Now, let's add styles to make the widget intuitive and visually appealing. We'll use scoped class names to avoid conflicts when embedded in external sites.


.bulk-compressor-widget {
  font-family: system-ui, -apple-system, sans-serif;
  max-width: 720px;
  margin: 2rem auto;
  padding: 1.5rem;
  border-radius: 12px;
  background: #f9fafb;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}

.bulk-compressor-widget .drop-zone {
  border: 2px dashed #cbd5e1;
  border-radius: 10px;
  padding: 2.5rem;
  text-align: center;
  color: #64748b;
  transition: background 0.2s ease, border-color 0.2s ease;
  cursor: pointer;
}

.bulk-compressor-widget .drop-zone.dragover {
  background: #eef2ff;
  border-color: #6366f1;
  color: #4f46e5;
}

.bulk-compressor-widget .browse-link {
  color: #4f46e5;
  font-weight: 600;
  text-decoration: underline;
}

.bulk-compressor-widget .controls {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-top: 1.5rem;
}

.bulk-compressor-widget button {
  padding: 0.75rem 1.25rem;
  border: none;
  border-radius: 8px;
  background: #4f46e5;
  color: white;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s ease, opacity 0.2s ease;
}

.bulk-compressor-widget button:disabled {
  background: #c7d2fe;
  cursor: not-allowed;
}

.bulk-compressor-widget button:not(:disabled):hover {
  background: #4338ca;
}

.bulk-compressor-widget .preview-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 0.75rem;
  margin-top: 1.5rem;
}

.bulk-compressor-widget .preview-grid .item {
  position: relative;
  border-radius: 8px;
  overflow: hidden;
  background: #fff;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
}

.bulk-compressor-widget .preview-grid img {
  display: block;
  width: 100%;
  height: 120px;
  object-fit: cover;
}

.bulk-compressor-widget .preview-grid .meta {
  font-size: 0.75rem;
  padding: 0.4rem 0.6rem;
  color: #475569;
}

Understanding the CSS


  • Scoped selectors (.bulk-compressor-widget ...) prevent style collisions in third-party site integrations.
  • Drag state styling (.dragover) gives users clear visual feedback.
  • Grid layout for the preview grid uses auto-fill and minmax to adapt responsively across screen sizes.
  • Disabled button states prevent user errors before files are loaded.

Step 3: Add the JavaScript Logic


Here's where the compression actually happens. We'll use the HTML5 Canvas API to redraw images at reduced quality and JSZip to bundle results.


<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js"></script>
<script>
(function () {
  const dropZone = document.getElementById('dropZone');
  const fileInput = document.getElementById('fileInput');
  const qualitySlider = document.getElementById('qualitySlider');
  const qualityValue = document.getElementById('qualityValue');
  const compressBtn = document.getElementById('compressBtn');
  const downloadBtn = document.getElementById('downloadBtn');
  const previewGrid = document.getElementById('previewGrid');

  let selectedFiles = [];
  let compressedBlobs = [];

  // Update slider label
  qualitySlider.addEventListener('input', () => {
    qualityValue.textContent = qualitySlider.value + '%';
  });

  // Drag & drop handlers
  ['dragenter', 'dragover'].forEach(evt =>
    dropZone.addEventListener(evt, e => {
      e.preventDefault();
      dropZone.classList.add('dragover');
    })
  );

  ['dragleave', 'drop'].forEach(evt =>
    dropZone.addEventListener(evt, e => {
      e.preventDefault();
      dropZone.classList.remove('dragover');
    })
  );

  dropZone.addEventListener('drop', e => {
    const files = Array.from(e.dataTransfer.files).filter(file =>
      file.type.startsWith('image/')
    );
    addFiles(files);
  });

  dropZone.addEventListener('click', () => fileInput.click());

  fileInput.addEventListener('change', () => {
    addFiles(Array.from(fileInput.files));
  });

  function addFiles(files) {
    selectedFiles = selectedFiles.concat(files);
    renderPreviews();
    compressBtn.disabled = selectedFiles.length === 0;
  }

  function renderPreviews() {
    previewGrid.innerHTML = '';
    selectedFiles.forEach((file, index) => {
      const item = document.createElement('div');
      item.className = 'item';

      const img = document.createElement('img');
      img.src = URL.createObjectURL(file);
      img.onload = () => URL.revokeObjectURL(img.src);

      const meta = document.createElement('div');
      meta.className = 'meta';
      meta.textContent = `${(file.size / 1024).toFixed(0)} KB`;

      item.appendChild(img);
      item.appendChild(meta);
      previewGrid.appendChild(item);
    });
  }

  function compressImage(file, quality) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => {
        const img = new Image();
        img.onload = () => {
          const canvas = document.createElement('canvas');
          canvas.width = img.width;
          canvas.height = img.height;

          const ctx = canvas.getContext('2d');
          ctx.drawImage(img, 0, 0);

          canvas.toBlob(
            blob => blob ? resolve(blob) : reject(new Error('Compression failed')),
            'image/jpeg',
            quality
          );
        };
        img.onerror = reject;
        img.src = reader.result;
      };
      reader.onerror = reject;
      reader.readAsDataURL(file);
    });
  }

  compressBtn.addEventListener('click', async () => {
    compressBtn.disabled = true;
    compressBtn.textContent = 'Compressing...';
    compressedBlobs = [];

    const quality = parseInt(qualitySlider.value, 10) / 100;

    for (const file of selectedFiles) {
      try {
        const blob = await compressImage(file, quality);
        compressedBlobs.push({ name: file.name.replace(/\.\w+$/, '') + '.jpg', blob });
      } catch (err) {
        console.error('Failed to compress', file.name, err);
      }
    }

    compressBtn.textContent = 'Compress Images';
    compressBtn.disabled = false;
    downloadBtn.disabled = compressedBlobs.length === 0;
  });

  downloadBtn.addEventListener('click', async () => {
    const zip = new JSZip();
    compressedBlobs.forEach(({ name, blob }) => zip.file(name, blob));

    const content = await zip.generateAsync({ type: 'blob' });
    const url = URL.createObjectURL(content);

    const a = document.createElement('a');
    a.href = url;
    a.download = 'compressed-images.zip';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  });
})();
</script>

Understanding the JavaScript


  • File Handling: The FileReader API reads files as base64 data URLs, which can then be loaded into an Image object.
  • Canvas Compression: By drawing the image onto a canvas and calling toBlob() with a specified quality parameter (0.0 to 1.0), the browser re-encodes the image at a lower size.
  • Async/Await: Wrapping the compression in a Promise ensures files are processed sequentially, preventing memory spikes with large batches.
  • JSZip Integration: Compressed blobs are pushed into a ZIP archive, then generated as a downloadable blob URL.
  • Memory Management: Object URLs created via URL.createObjectURL() are revoked after use to prevent memory leaksβ€”an especially important consideration for long-lived single-page applications.

Wrapping Up


You've just built a fully client-side bulk image compressor that requires zero backend infrastructure. This pattern is increasingly relevant in 2026, as browsers push newer APIs like createImageBitmap and WebCodecs that can further accelerate image processing.


From here, you could extend the widget with:


  • WebP/AVIF support using canvas.toBlob('image/webp') or canvas.toBlob('image/avif') for even smaller file sizes.
  • Resizing controls to cap maximum dimensions.
  • Progress indicators for large batches using Promise.all with progress tracking.
  • Drag-to-reorder in the preview grid before compression.

By keeping everything in the browser, you deliver a faster, more private, and more cost-effective toolβ€”one that any CMS, landing page, or internal dashboard can host with a single snippet.

via FreeCodeCamp

Related