Generate and Download Files in the Browser: Blob + download
"Can we get an export button?" doesn't have to become a backend ticket. If the data is already in the browser - a filtered table, editor content, app settings - the browser can mint the file itself:
function downloadFile(content, filename, type = 'text/plain') {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename; // the attribute that makes it a download, and names it
a.click();
URL.revokeObjectURL(url); // release the blob's memory reference
}
Three moving parts: a Blob wraps your string (or binary data) as a file-like object; createObjectURL gives it a temporary blob: URL; the download attribute on a programmatic link turns navigation into a save-file with your chosen name. The revoke matters - blob URLs pin their data in memory until released or the page dies.
The CSV export everyone eventually writes
function toCsv(rows) {
const escape = (v) => {
const s = String(v ?? '');
return /[",\n]/.test(s) ? '"' + s.replaceAll('"', '""') + '"' : s;
};
return rows.map(row => row.map(escape).join(',')).join('\r\n');
}
const csv = toCsv([
['Order', 'Email', 'Total'],
...orders.map(o => [o.number, o.email, o.total]),
]);
downloadFile('' + csv, 'orders.csv', 'text/csv;charset=utf-8');
Two details separate this from naive joins: the escaping function (quotes around anything containing commas, quotes or newlines, with internal quotes doubled - the actual CSV spec, which naive exports violate the first time a customer is named O'Brien, Inc.), and the byte-order mark, without which Excel opens UTF-8 CSVs as mojibake. That BOM prefix has ended more "the export is broken" tickets than any other single character.
Other flavors, same pattern
// Settings backup
downloadFile(JSON.stringify(settings, null, 2), 'backup.json', 'application/json');
// Canvas to image
canvas.toBlob((blob) => {
const a = Object.assign(document.createElement('a'),
{ href: URL.createObjectURL(blob), download: 'chart.png' });
a.click();
URL.revokeObjectURL(a.href);
}, 'image/png');
Boundaries
- The download attribute only applies to same-origin and blob:/data: URLs - you can't force-rename a cross-origin file; the browser ignores it.
- Client memory is the ceiling: tens of megabytes fine, gigabytes no. Big exports stay server-side, streamed with PHP generators.
- The reverse trip (server-generated protected files) has its own tricks - X-Sendfile on the PHP side.
For the everyday "export what I'm looking at" though, no round-trip beats zero server code, instant response, and data that never leaves the machine - which your privacy policy will appreciate too.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.