Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

Generate and Download Files in the Browser: Blob + download

C
Carlo Fontanos
· 2 min read

"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.

C
Written by Carlo Fontanos

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.

Message sent!

Your details are only used to reply to you.

Keep reading