Trigger a File Download from a Blob
Generating a CSV, JSON export, or screenshot client-side and saving it without a server round-trip is a five-line trick: build a `Blob`, mint an object URL, click a hidden `<a download>`, and revoke the URL. This snippet covers the canonical helper, a JSON export wrapper, and the cleanup pattern that prevents memory leaks during long-running sessions.
699 views
16
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
const blob = new Blob(['hello, world'], { type: 'text/plain' });
downloadBlob(blob, 'hello.txt');
console.log('download triggered');URL.createObjectURL(blob) mints a blob:... URL that points at the in-memory data and can be assigned to any URL-accepting attribute. Programmatic .click() on a hidden <a> with the download attribute is the only cross-browser way to trigger a save dialog from JS. Inserting the anchor into the DOM is required in some browsers (Firefox in particular); pulling it back out keeps the page tree clean. Calling URL.revokeObjectURL immediately after the click frees the underlying blob reference so the GC can reclaim memory.
function downloadJson(data, filename, { pretty = true } = {}) {
const text = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
const blob = new Blob([text], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
downloadJson({ ok: true, items: [1, 2, 3] }, 'export.json');
console.log('json export triggered');Wrapping the blob helper with a JSON serialiser gives you a one-line export for state.toJSON() or any structured payload. Defaulting to pretty-print with two-space indent is friendlier for users who open the file in a text editor; flip the flag for machine-to-machine downloads to save bytes. Setting the MIME type to application/json makes some browsers (and OSes) preview the file correctly. Add a BOM (\uFEFF prefix) only if Excel users are downloading and need correct UTF-8 detection.
function downloadBlobSafe(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
// Some older browsers cancel the download if the URL is revoked too quickly.
// Defer the cleanup so the click has time to commit.
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 0);
}
downloadBlobSafe(new Blob(['safe path'], { type: 'text/plain' }), 'safe.txt');
console.log('safe download triggered');Some older Chromium and embedded webview combinations cancel the download if the object URL is revoked synchronously after click(), because the actual disk-write is queued on the renderer's IO thread. Wrapping the cleanup in a zero-delay setTimeout lets the navigation settle before the URL is freed. Adding rel = 'noopener' is defence-in-depth for the rare case where the link does navigate (it should not, with download, but a misconfigured CSP or extension can interfere). For production, ship the deferred-revoke version.
