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:
- Track selection as a set of IDs, not as file objects.
- Build the
filesarray from the selection at click time. - Disable the button when the selection is empty — an empty list fails synchronously by design.
The complete example
import { useState } from 'react';
import { useEazip } from '@eazip/react';
type Item = { id: string; name: string; url: string };
export function SelectableFileList({ items }: { items: Item[] }) {
const [selected, setSelected] = useState<Set<string>>(new Set());
const zip = useEazip();
function toggle(id: string) {
setSelected((prev) => {
const next = new Set(prev);
if (!next.delete(id)) next.add(id);
return next;
});
}
function downloadSelected() {
const files = items
.filter((item) => selected.has(item.id))
.map((item) => ({ url: item.url, filename: item.name }));
zip.download({ files, zipName: 'selected-files.zip' });
}
return (
<>
<ul>
{items.map((item) => (
<li key={item.id}>
<label>
<input
type="checkbox"
checked={selected.has(item.id)}
onChange={() => toggle(item.id)}
/>
{item.name}
</label>
</li>
))}
</ul>
<button
disabled={selected.size === 0 || zip.isBusy}
onClick={downloadSelected}
>
Download selected ({selected.size})
</button>
</>
);
}Render <EazipTray /> once near the app root and progress, cancel, and
the finished download are handled for you.
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
downloadSelectedworks unchanged. - Name entries deliberately.
filenameis what users see after unzipping; forward slashes create folders, so a selection spanning categories can unzip asinvoices/…andphotos/…. - 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
urlvalues 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.
Download Multiple Files as a ZIP in React
Build a production "Download all" button with @eazip/react — fetch the file list, start the job with useEazip(), and let <EazipTray /> handle progress, partial results, and retry.
Show ZIP Progress and Let Users Cancel or Retry
Build a real download experience around a ZIP job — live progress from startZip(), a cancel button via abort(), and a retry that handles failed and partial outcomes correctly.