Blob URLs in JavaScript: make a file in the browser and give it an address

A Blob is a file that exists only in the page's memory. URL.createObjectURL gives it a short blob: address, so a link can download it and an image or video can show it.

A blob URL is a temporary address for data that lives in the page. Put text, CSV or an image in a Blob, pass it to URL.createObjectURL(), and you get a short blob: URL.

Use it in a link's href with a download attribute to save a file, or in an img or video src to show it.

Try it first. Type something, name the file, and press the button.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Text to file</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 4px; }
  textarea, input { width: 100%; box-sizing: border-box; font: 14px ui-monospace, Consolas, monospace;
    padding: 8px; border: 1px solid #cfd4dc; border-radius: 8px; background: #fff; }
  textarea { height: 90px; resize: vertical; }
  .row { display: flex; gap: 8px; margin-top: 10px; }
  .row input { flex: 1; }
  button { padding: 8px 14px; border: 0; border-radius: 8px; background: #1d4ed8; color: #fff; font-weight: 600; cursor: pointer; }
  #out { margin-top: 12px; padding: 10px 12px; border-radius: 8px; background: #fff; border: 1px solid #e1e4ea; font-size: 13px; line-height: 1.6; }
  #out code { word-break: break-all; }
</style>
</head>
<body>
<label for="text">File contents</label>
<textarea id="text">Hello from the browser.
This file was never on a server.</textarea>
<div class="row">
  <input id="name" value="note.txt" aria-label="File name">
  <button id="make">Make file</button>
</div>
<div id="out">Press <b>Make file</b>.</div>

<script>
  const out = document.getElementById('out');
  let url = null;  // the current blob URL, kept so it can be revoked

  document.getElementById('make').addEventListener('click', () => {
    if (url) URL.revokeObjectURL(url);  // free the previous file

    const text = document.getElementById('text').value;
    const blob = new Blob([text], { type: 'text/plain' });
    url = URL.createObjectURL(blob);

    const name = document.getElementById('name').value || 'note.txt';
    out.innerHTML =
      'URL: <code>' + url + '</code><br>' +
      'Size: ' + blob.size + ' bytes · Type: ' + blob.type + '<br>' +
      '<a href="' + url + '" download="' + name + '">Download ' + name + '</a>';
  });
</script>
</body>
</html>
Text becomes a Blob, the Blob gets a blob: URL, and the link offers it as a file.

The four steps: Blob, URL, use, revoke

Every blob URL follows the same path from data to file.

From data to a file: wrap it, give it an address, use the address, then release it.
From data to a file: wrap it, give it an address, use the address, then release it.
  1. Wrap the content. new Blob([text], { type: 'text/plain' }) takes an array of parts (strings, other Blobs, typed arrays) and a MIME type.
  2. Create the address. URL.createObjectURL(blob) returns a URL such as blob:https://example.com/5f3c9a2e-....
  3. Use it. Any attribute that takes a URL accepts it: href, src, even fetch().
  4. Revoke it. URL.revokeObjectURL(url) tells the browser the address is no longer needed.

The core of the first example is only this:

const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);

link.href = url;
link.download = 'note.txt';   // suggested file name

Strings are stored as UTF-8, and blob.size is the length in bytes, not characters. An accented letter such as é takes two bytes in UTF-8, which is why "abc é" is 6 bytes.

Previews: show a picked file without uploading it

A File from an <input type="file"> or a drop is already a Blob. That means it can go straight into createObjectURL:

input.addEventListener('change', () => {
  const file = input.files[0];
  if (preview.src) URL.revokeObjectURL(preview.src);
  preview.src = URL.createObjectURL(file);   // works for img, video and audio
});

The picture appears at once, because nothing is read or converted. The whole drop-a-file flow, with size checks and upload, is in drag and drop file upload.

blob: URL vs data: URL

Both put your own data where a URL is expected, but they work differently. A blob URL points at bytes in memory. A data URI writes the bytes into the address itself.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>blob: vs data: URL</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .ctrl { font-size: 14px; margin-bottom: 12px; }
  .ctrl input { vertical-align: middle; width: 160px; }
  .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
  .box { background: #fff; border: 1px solid #e1e4ea; border-radius: 10px; padding: 10px; min-width: 0; }
  .box h3 { margin: 0 0 6px; font: 700 14px ui-monospace, Consolas, monospace; }
  .box img { display: block; width: 100%; max-width: 140px; aspect-ratio: 1; margin: 0 auto 8px; background: #f8fafc; border-radius: 6px; }
  .len { font-size: 13px; }
  .len b { font-size: 18px; }
  .url { font: 11px/1.4 ui-monospace, Consolas, monospace; color: #6b7280; word-break: break-all;
    max-height: 60px; overflow: hidden; margin-top: 6px; }
</style>
</head>
<body>
<div class="ctrl">
  <label for="n">Dots in the picture: <b id="nv">5</b></label>
  <input id="n" type="range" min="1" max="60" value="5">
</div>
<div class="grid">
  <div class="box">
    <h3>blob:</h3>
    <img id="blobImg" alt="Picture loaded from a blob URL">
    <div class="len">URL length: <b id="blobLen"></b> characters</div>
    <div class="url" id="blobUrl"></div>
  </div>
  <div class="box">
    <h3>data:</h3>
    <img id="dataImg" alt="Picture loaded from a data URL">
    <div class="len">URL length: <b id="dataLen"></b> characters</div>
    <div class="url" id="dataUrl"></div>
  </div>
</div>

<script>
  const $ = (id) => document.getElementById(id);
  let blobUrl = null;

  // An SVG picture written as a string: n coloured dots
  function makeSvg(n) {
    let dots = '';
    for (let i = 0; i < n; i++) {
      const x = 10 + (i * 37) % 80, y = 10 + (i * 53) % 80;
      dots += '<circle cx="' + x + '" cy="' + y + '" r="7" fill="hsl(' + i * 29 + ',70%,55%)"/>';
    }
    return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">' + dots + '</svg>';
  }

  function draw() {
    const n = +$('n').value;
    $('nv').textContent = n;
    const svg = makeSvg(n);

    // blob: a short address that points at the bytes in memory
    if (blobUrl) URL.revokeObjectURL(blobUrl);
    blobUrl = URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }));

    // data: the bytes written into the address itself
    const dataUrl = 'data:image/svg+xml,' + encodeURIComponent(svg);

    $('blobImg').src = blobUrl;
    $('dataImg').src = dataUrl;
    $('blobLen').textContent = blobUrl.length;
    $('dataLen').textContent = dataUrl.length;
    $('blobUrl').textContent = blobUrl;
    $('dataUrl').textContent = dataUrl;
  }

  $('n').addEventListener('input', draw);
  draw();
</script>
</body>
</html>
The same SVG picture as a blob: URL and as a data: URL. Add dots and watch only one of them grow.

Move the slider. The picture gets bigger, the data URL gets longer, and the blob URL keeps the same length.

A blob URL is a pointer. A data URL carries the whole file inside the text.
A blob URL is a pointer. A data URL carries the whole file inside the text.
blob: URL data: URL
Made with URL.createObjectURL(blob) A string, or FileReader.readAsDataURL
Length Short, whatever the file size Grows with the file; base64 turns every 3 bytes into 4 characters
Survives a reload No Yes, it is plain text
Can be saved in the HTML No Yes
Needs cleanup URL.revokeObjectURL No

Use a blob URL for downloads and previews made while the page is open. Use a data URL when the content has to be stored in the file, as in base64 images in HTML.

How long a blob URL lives

A blob URL belongs to the page that created it. It stops working when that page reloads or closes, or when you revoke it.

Where a blob URL works, and where the same address leads nowhere.
Where a blob URL works, and where the same address leads nowhere.

This is also why a blob URL cannot be shared. Pasted into a chat, it is an address for memory on your device. The other person's browser has no such Blob, so the link fails.

Revoking matters when a page makes many files. Each createObjectURL call keeps its Blob in memory until the page closes.

An exporter that makes a new file on every click should revoke the old URL first, which the finished example below does. An image that already loaded keeps showing after its URL is revoked.

Reading a Blob back

To check what is inside a Blob, read it:

const text = await blob.text();          // as a string (UTF-8)
const bytes = await blob.arrayBuffer();  // as raw bytes

// Older API, same result with an event
const reader = new FileReader();
reader.onload = () => console.log(reader.result);
reader.readAsText(blob);

blob.text() returns a promise and is the short way. FileReader also has readAsDataURL, which turns a Blob into a data URL when you do need one. For JSON files, pass the text to JSON.parse, covered in JSON.parse.

A finished example: export a table as CSV or JSON

Here the data comes from the page. Edit any cell, then export. The file is built from the table, shown back with blob.text(), and offered as a download.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Table to CSV and JSON</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  p.hint { margin: 0 0 8px; font-size: 13px; color: #4b5563; }
  table { width: 100%; border-collapse: collapse; background: #fff; font-size: 14px; }
  th, td { border: 1px solid #d5d9e0; padding: 6px 8px; text-align: left; }
  th { background: #eef1f5; }
  td:focus { outline: 2px solid #1d4ed8; outline-offset: -2px; }
  .bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 10px 0; font-size: 13px; }
  button { padding: 7px 12px; border: 0; border-radius: 8px; background: #1d4ed8; color: #fff; font-weight: 600; cursor: pointer; }
  #links { font-size: 13px; line-height: 1.7; }
  #links code { font-size: 11.5px; word-break: break-all; color: #6b7280; }
  pre { margin: 6px 0 0; padding: 8px 10px; background: #111827; color: #e5e7eb; border-radius: 8px;
    font-size: 12px; max-height: 110px; overflow: auto; white-space: pre-wrap; }
</style>
</head>
<body>
<p class="hint">Click a cell to edit it, then export.</p>
<table id="t">
  <thead><tr><th>Item</th><th>Qty</th><th>Note</th></tr></thead>
  <tbody>
    <tr><td contenteditable>Coffee beans</td><td contenteditable>2</td><td contenteditable>Dark, "house" roast</td></tr>
    <tr><td contenteditable>Oat milk</td><td contenteditable>6</td><td contenteditable>Barista, 1 L</td></tr>
    <tr><td contenteditable>Filters</td><td contenteditable>1</td><td contenteditable>Size 4</td></tr>
  </tbody>
</table>
<div class="bar">
  <button id="csv">Export CSV</button>
  <button id="json">Export JSON</button>
  <label><input type="checkbox" id="bom"> Add BOM to CSV</label>
</div>
<div id="links">Nothing exported yet.</div>
<pre id="preview" hidden></pre>

<script>
  const $ = (id) => document.getElementById(id);
  let url = null, revoked = 0;

  // Read the table into an array of rows (header first)
  function rows() {
    return [...$('t').rows].map((tr) => [...tr.cells].map((c) => c.textContent.trim()));
  }

  // CSV: wrap a cell in quotes if it has a comma, quote or line break; double inner quotes
  function toCsv(data) {
    const cell = (v) => /[",\n]/.test(v) ? '"' + v.replace(/"/g, '""') + '"' : v;
    return data.map((r) => r.map(cell).join(',')).join('\r\n');
  }

  function toJson(data) {
    const [head, ...body] = data;
    const objs = body.map((r) => Object.fromEntries(head.map((h, i) => [h, r[i]])));
    return JSON.stringify(objs, null, 2);
  }

  async function exportFile(text, type, name) {
    if (url) { URL.revokeObjectURL(url); revoked++; }  // the old file is no longer needed
    const blob = new Blob([text], { type });
    url = URL.createObjectURL(blob);

    $('links').innerHTML =
      '<a href="' + url + '" download="' + name + '">Download ' + name + '</a> · ' +
      blob.size + ' bytes · ' + type + '<br><code>' + url + '</code><br>' +
      'Old URLs revoked: ' + revoked;

    // Read the blob back to show what is inside the file
    $('preview').hidden = false;
    $('preview').textContent = await blob.text();
  }

  $('csv').addEventListener('click', () => {
    const bom = $('bom').checked ? '' : '';
    exportFile(bom + toCsv(rows()), 'text/csv', 'shopping.csv');
  });
  $('json').addEventListener('click', () => {
    exportFile(toJson(rows()), 'application/json', 'shopping.json');
  });
</script>
</body>
</html>
Edit the table, export CSV or JSON. Each export revokes the previous blob URL.
  • CSV quoting: a cell with a comma, a quote or a line break is wrapped in quotes, and inner quotes are doubled. The note cell becomes "Dark, ""house"" roast".
  • JSON: the header row becomes the keys, and JSON.stringify(rows, null, 2) makes it readable.
  • Types: text/csv for CSV, application/json for JSON.
  • BOM: the checkbox adds \uFEFF to the start. The file grows by 3 bytes. Some spreadsheet apps need it to read accented letters in a CSV correctly.

When it does not work

What you see Cause Fix
The link opens the file instead of saving it The URL is on another origin, so download is ignored Fetch the data (the server must allow it), make a Blob, link to its blob URL
Clicking Download does nothing The page runs in a sandboxed frame without allow-downloads Open the page directly, or allow downloads on the frame
The link is dead after a reload or in a message Blob URLs live only while the page that made them is open Make the Blob again; use a data URL if it must be stored
Memory keeps growing as you export Old blob URLs were never revoked Call URL.revokeObjectURL before making the next one
An SVG blob does not show in an img The Blob has no type or the wrong one Use { type: 'image/svg+xml' }
Accented letters look broken in a spreadsheet The app did not read the CSV as UTF-8 Start the text with \uFEFF (a byte order mark)

The sandbox row is covered in more detail in the iframe sandbox attribute.

A page that makes files is easier to try than to describe. Send it as a link and the scripts run, so the people you send it to can edit the table and download the CSV themselves.

To do that, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written, its scripts run, and downloads from blob URLs work there. If you change the code later, the same link shows the new version.

Questions people ask

What is a blob URL?

An address that starts with blob: and points at a Blob or File held in the browser's memory, for example blob:https://example.com/5f3c9a2e-... It is made by URL.createObjectURL and works like any other URL in href and src, but only in the browser that created it.

How do I download a file made in JavaScript?

Wrap the content in a Blob with a type, create a URL with URL.createObjectURL, and put it in the href of a link that has a download attribute. The download value becomes the suggested file name. Clicking the link saves the file.

Can I send a blob URL to someone else?

No. The address points at memory in your browser. On another device or browser it leads nowhere, and even in your own browser it stops working when the page that made it is reloaded or closed, or when the URL is revoked.

When should I call URL.revokeObjectURL?

When the page no longer needs that address: before replacing it with a new export, or when an image preview is removed. Until then the browser keeps the Blob in memory. An image that already loaded from the URL stays on screen after revoking.

Is a blob URL better than a data URL?

For large files and one-off downloads, usually yes: the address stays short and nothing is converted to text. A data URL is better when the content must be saved inside the HTML itself, because it survives a reload.

Keep reading