Eazip.js
EazipGitHub
Guides

Let Users Download Selected Files as a ZIP

Wire checkboxes to a ZIP download — track a selection set, build the files array from it, and disable the button when nothing is selected.

A file list with checkboxes and a "Download selected" button is the grown-up sibling of "download all": same ZIP machinery, plus a selection state that decides what goes in. This guide wires the two together.

The whole feature is three rules:

  1. Track selection as a set of IDs, not as file objects.
  2. Build the files array from the selection at click time.
  3. Disable the button when the selection is empty — an empty list fails synchronously by design.

The complete example

selected-files.ts
import { createZip } from '@eazip/core';

const selected = new Set<string>();

document.querySelectorAll<HTMLInputElement>('input[data-file-id]').forEach(
  (box) => {
    box.addEventListener('change', () => {
      if (box.checked) selected.add(box.dataset.fileId!);
      else selected.delete(box.dataset.fileId!);
      button.disabled = selected.size === 0;
    });
  },
);

const button = document.querySelector<HTMLButtonElement>('#download-selected')!;

button.addEventListener('click', async () => {
  const files = catalog
    .filter((item) => selected.has(item.id))
    .map((item) => ({ url: item.url, filename: item.name }));

  const result = await createZip({ files, zipName: 'selected-files.zip' });
  result.download();
});

catalog is whatever your page already knows about the listed files — { id, name, url } entries from your API response.

Details that make it feel right

  • "Select all" is just state. Set the selection to every ID (or clear it) and the same downloadSelected works unchanged.
  • Name entries deliberately. filename is what users see after unzipping; forward slashes create folders, so a selection spanning categories can unzip as invoices/… and photos/….
  • One file selected still makes a ZIP. If you'd rather hand over the bare file when exactly one is selected, branch before calling createZip — both behaviors are defensible; pick one and keep it.
  • Private files: the url values should be short-lived signed URLs from your server, minted for the listed files. The per-platform signing code lives in the storage guides (S3, R2, Supabase).

FAQ

Why disable the button instead of handling an empty-list error?

An empty files value fails synchronously — it's a normal UI state, not an exceptional one, so the interface should prevent it rather than catch it.

The user changes the selection while a ZIP is running — what happens?

Nothing, to the running job: it captured the array at click time. Keep the button disabled with isBusy (React) while a job runs so a second job isn't stacked on the first.

Can selected local files (from a picker) mix with remote URLs?

Yes — files accepts File/Blob objects and URL entries in one array. See Inputs and sources for every accepted shape.

What if a selected file's URL has expired by click time?

The job keeps the usable output by default and reports the failure — partial output with a skipped count rather than a dead button. See Partial results and errors.