Create ZIP archives in the browser with JSZip
Pass the browser’s File objects to JSZip, generate a ZIP Blob, and give that Blob to a download
link. The complete page below lets users choose or drop several local files and download them as
archive.zip, with progress feedback and a limit on total input size.
This example does not upload selected files or require a compression server. It loads JSZip from a CDN, so an internet connection is needed to load the library. The compression work and its memory cost stay in the browser; this approach suits modest collections of files that are already on the user’s device.
Run the complete browser example
Save this as a new index.html in an empty folder, then open it in desktop Chrome. There is no build
step or local server. The script pins JSZip 3.10.2; this example was tested in Chrome 153 on macOS.
Choose individual files with the picker, or drop them inside the fieldset. Each selection replaces the previous set, whose names appear below the picker. Folder traversal is not included. Click Create ZIP, wait for the download request, then open the ZIP from your browser’s downloads.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Create a ZIP with JSZip</title>
</head>
<body>
<h1>Create a ZIP</h1>
<fieldset id="dropZone">
<legend>Choose files or drop them here</legend>
<label for="fileInput">Files to archive</label>
<input type="file" id="fileInput" multiple />
<p id="selection">No files selected.</p>
<button type="button" id="zipButton">Create ZIP</button>
</fieldset>
<p id="progress" role="status">Select files totaling at most 100 MiB.</p>
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.2/dist/jszip.min.js"></script>
<script>
const fileInput = document.getElementById('fileInput')
const dropZone = document.getElementById('dropZone')
const selection = document.getElementById('selection')
const progress = document.getElementById('progress')
let selectedFiles = []
let busy = false
function selectFiles(files) {
if (busy) return
selectedFiles = Array.from(files)
// The names below remain visible; clearing the picker allows reselection.
fileInput.value = ''
selection.textContent = selectedFiles.length
? `Selected files: ${selectedFiles.map((file) => file.name).join(', ')}`
: 'No files selected.'
progress.textContent = 'Ready to create ZIP.'
}
fileInput.addEventListener('change', () => selectFiles(fileInput.files))
dropZone.addEventListener('dragover', (event) => {
event.preventDefault()
event.dataTransfer.dropEffect = busy ? 'none' : 'copy'
})
dropZone.addEventListener('drop', (event) => {
event.preventDefault()
selectFiles(event.dataTransfer.files)
})
async function createZip() {
if (busy) return
const files = selectedFiles.slice()
if (files.length === 0) {
progress.textContent = 'Select or drop files first.'
return
}
if (typeof JSZip === 'undefined') {
progress.textContent = 'JSZip could not load. Check your connection and reload.'
return
}
const totalSize = files.reduce((sum, file) => sum + file.size, 0)
if (totalSize > 100 * 1024 * 1024) {
progress.textContent = 'Select at most 100 MiB of files.'
return
}
const names = new Set()
for (const file of files) {
if (names.has(file.name)) {
progress.textContent = `Duplicate filename: ${file.name}. Rename it first.`
return
}
names.add(file.name)
}
busy = true
dropZone.disabled = true
progress.textContent = 'Generating ZIP…'
try {
const zip = new JSZip()
for (const file of files) zip.file(file.name, file)
const blob = await zip.generateAsync(
{
type: 'blob',
compression: 'DEFLATE',
compressionOptions: { level: 6 },
},
({ percent }) => {
progress.textContent = `Generating ZIP: ${Math.round(percent)}%`
},
)
const url = URL.createObjectURL(blob)
// Leave time for the browser to consume the URL before releasing it.
setTimeout(() => URL.revokeObjectURL(url), 60_000)
const link = document.createElement('a')
link.href = url
link.download = 'archive.zip'
document.body.appendChild(link)
link.click()
link.remove()
progress.textContent = 'ZIP download requested. Check your browser’s downloads.'
} catch {
progress.textContent = 'Could not create ZIP. Reselect the files and try again.'
} finally {
busy = false
dropZone.disabled = false
}
}
document.getElementById('zipButton').addEventListener('click', createZip)
</script>
</body>
</html>
For a quick check, select a text file and an image with different names. The downloaded ZIP should
contain both at its root, with their contents unchanged. Generate it again to request another
download. archive.zip is a suggested name: the browser controls the save location and any rename
or replacement prompt when that name already exists. The page does not force an overwrite.
Add files directly and report generation progress
A file input and a file drop both provide File objects through the File API. JSZip’s
file(name, data) method
accepts these directly because a File is a kind of Blob. You do not need a separate FileReader
wrapper or a base64 conversion to add local files to the archive.
generateAsync()
returns a promise for the completed archive. Here it produces a Blob and uses DEFLATE at level six.
The callback’s percent describes archive generation, not a download’s progress. Once the Blob is
ready, a temporary URL makes it available to the link; the timer releases that URL after the
download request.
“ZIP download requested” is deliberately different from “saved.” The link’s
download property
does not confirm that a download happened. Browser settings can block it, or the user can cancel
the save dialog. Check the downloads panel if no file appears.
Keep one ZIP job active
The busy guard is set before compression starts. While the promise is pending, the fieldset
disables the picker and button, and selectFiles() ignores new drops or change events. The job
keeps its original selection, so a second click cannot start another archive or replace the progress
message. After success or failure, finally restores the controls. To use a different selection,
wait for the current job to finish, then select or drop the new files.
JSZip updates an existing entry when the same name is added again. The duplicate check prevents one selected file from silently replacing another, which matters when files from different folders share a basename. Rename one before retrying. Names that differ only in letter case can still collide when extracted onto a filesystem that ignores case.
If the CDN cannot load, the page asks you to check the connection. If reading or compression fails, it asks you to reselect the files. A failed job does not request a download. For a page that must work offline, serve the pinned JSZip bundle with your own page instead of relying on the CDN.
Handling large files and performance considerations
The 100 MiB check is an example input policy, not a browser capacity guarantee. JSZip’s
generateAsync() holds the complete result in memory,
and reading and compressing the inputs needs memory too. Many small files also add overhead.
Choose a lower limit if your target devices cannot comfortably handle that workload.
DEFLATE can shrink repetitive text, but already compressed images and videos may see little
benefit. If you only need to bundle those files, JSZip also supports compression: 'STORE', which
skips compression. Moving compression into a Web Worker can keep that work off the page’s main
thread, but it does not make generateAsync({ type: 'blob' }) stream its result to disk. Large
exports need a different memory and output strategy.
Browser compatibility
The verified target here is desktop Chrome on macOS. Other browsers need the File API, promises,
async/await, Blob URLs, and support for downloading from those URLs. Keep the file picker for
devices without file dragging, and test selection, error recovery, and actual downloads in each
browser you intend to support. JSZip’s library support alone does not establish that the whole
page’s download flow works there.
