Eazip.js
EazipGitHub
Guides

Download Multiple Files at Once in JavaScript

Why the loop of anchor clicks fails, what browsers actually allow, and the reliable pattern — packaging the files into one ZIP download client-side.

"Just trigger a download per file" is everyone's first idea, and browsers are built to defeat it. This page covers what actually happens when you try, and the pattern that works reliably: one ZIP, one download, built in the page.

Why the anchor loop fails

// The intuitive approach — and its problems
for (const url of urls) {
  const a = document.createElement('a');
  a.href = url;
  a.download = suggestedName(url);
  a.click();
}
  • Browsers throttle multi-download bursts. One user gesture earns roughly one download; the rest trip the popup/download blocker or arrive haphazardly, varying by browser and settings.
  • download renaming doesn't cross origins. For cross-origin URLs the attribute is ignored, so your carefully suggested filenames vanish.
  • The user gets N files, not a folder. Twenty loose files in Downloads/ — some duplicated as report (3).pdf — is the experience you were trying to avoid.
  • Sequential tricks (delays between clicks, iframe farms) trade one unreliability for another and still fail the folder problem.

The File System Access API (showDirectoryPicker) can write many files with permission, but it's Chromium-only and permission-gated — a fallback still has to exist.

The reliable pattern: one ZIP

Package the files client-side and trigger a single download — one user gesture, one file, real filenames, folder structure preserved:

import { createZip } from '@eazip/core';

const result = await createZip({
  files: [
    { url: '/reports/q1.pdf', filename: 'reports/q1.pdf' },
    { url: '/reports/q2.pdf', filename: 'reports/q2.pdf' },
    localNotesBlob && { file: localNotesBlob, filename: 'notes.txt' },
  ].filter(Boolean),
  zipName: 'documents.zip',
});

result.download();

Eazip.js is MIT-licensed, runs entirely in the page, and accepts files, blobs, and URLs mixed in one array. Remote URLs must be fetchable from the page — same-origin, public with CORS, or signed URLs from your server (Inputs and sources has the rules).

Building this in React? The complete component — button, progress tray, partial results — is in Download multiple files as a ZIP in React. Letting users pick which files? See Download selected files.

When a single file per click is actually fine

Honesty about the boundary: if the normal case is one or two files, a plain anchor per gesture works and needs no library. The ZIP pattern earns its place when "download all" means many files, when filenames and folder structure matter, or when the sources are a mix of local and remote.

And when the file set is huge — gigabytes, thousands of URLs — the browser itself becomes the wrong place to assemble it; the same call moves to managed Eazip with strategy: 'cloud' (Scale with Eazip).

FAQ

Can I download multiple files without zipping them?

Within limits: one download per real user gesture is dependable, and the File System Access API can write a folder in Chromium browsers after a permission prompt. Neither gives a portable "click once, get everything" — that's what the ZIP is for.

Do the files pass through a server?

No — on the Local path the page fetches the sources and builds the archive in memory. Nothing is uploaded anywhere.

Why do my cross-origin files lose their filenames with plain anchors?

The download attribute only applies same-origin (or to blob/data URLs). Inside a ZIP the names are yours unconditionally — each entry's filename is written into the archive.

What if one of the URLs fails?

The default keeps the usable output: the ZIP ships without the failed file and the failure is reported, rather than the whole download dying — see Partial results and errors.