Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

To download HTML in a browser, put the markup in a Blob, create a temporary blob: URL with URL.createObjectURL(), and click an anchor whose download attribute contains the suggested filename. If the page lives at a URL, fetch it first, verify response.ok, read the response as text, and pass that text to the same download function.

These are separate jobs: saving HTML you already have is local; retrieving HTML from another origin is subject to Fetch, same-origin rules, and CORS. The browser—not JavaScript—controls the final prompt, save location, and whether a filename suggestion is accepted.

Download HTML that your page already generated

This function accepts an HTML string, creates an HTML Blob, and starts a download without navigating away from the current page.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
function downloadHtml(html, filename = "page.html") {
  const blob = new Blob([html], { type: "text/html;charset=utf-8" });
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");

  link.href = url;
  link.download = filename;
  link.click();

  // Keep the URL alive long enough for the browser to start the download.
  setTimeout(() => URL.revokeObjectURL(url), 30_000);
}

const html = `<!doctype html>
<html lang="en">
  <head><meta charset="utf-8"><title>Saved page</title></head>
  <body><h1>Hello</h1></body>
</html>`;

downloadHtml(html, "saved-page.html");

The text/html;charset=utf-8 type tells the browser what the Blob contains. The .html extension makes the suggested file type clear to users and operating systems. A Blob URL is temporary and exists only in the browser context that created it.

Download the current document’s markup

If you want the DOM as it currently exists, serialize it before calling the function. This captures the current element tree, not necessarily every original response byte.

const html = "<!doctype html>n" + document.documentElement.outerHTML;
downloadHtml(html, "current-page.html");

Runtime state has limits: a canvas may not serialize its pixels into markup, event listeners are not represented, and resources referenced by relative URLs may resolve differently when the file is opened locally. If you need a self-contained archive, inline or separately save the required CSS, scripts, fonts, and images.

Fetch an HTML page from a URL, then save it

fetch() retrieves the response; it does not itself display a Save dialog. Read the response body as text, then reuse the Blob download path.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
async function downloadPage(url, filename = "page.html") {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status} ${response.statusText}`);
  }

  const html = await response.text();
  downloadHtml(html, filename);
}

downloadPage("/reports/weekly.html", "weekly.html")
  .catch(error => console.error("Download failed:", error));

For a same-origin URL, this normally works when the server returns the page to your document’s origin. For a cross-origin URL, the remote server must allow your request with an appropriate CORS policy. JavaScript cannot bypass a CORS denial. A request that appears in the Network panel can still be unreadable to page code if the response lacks permission for your origin.

Use the response Blob when you do not need to inspect text

async function downloadResponse(url, filename = "page.html") {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);

  const blob = await response.blob();
  const objectUrl = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = objectUrl;
  link.download = filename;
  link.click();
  setTimeout(() => URL.revokeObjectURL(objectUrl), 30_000);
}

This preserves the response’s media type when the server supplies one, although the filename you suggest should still end in .html for an HTML page. Use response.text() when you need to modify, sanitize, or inspect the markup first.

Rank #2
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option

Start the download from a user action

Call download code from a button click or another clear user gesture when possible. Browsers may apply popup or download protections to work started much later from an unrelated timer, especially when the operation opens a new window. A normal anchor download is not a silent arbitrary-path write: the browser chooses its Downloads folder, prompts according to user settings, and does not provide a general overwrite-in-place editor workflow.

document.querySelector("#save").addEventListener("click", () => {
  const html = "<!doctype html>n" + document.documentElement.outerHTML;
  downloadHtml(html, "page.html");
});

Use a button such as <button id="save" type="button">Download HTML</button>. Do not assume that setting download guarantees a particular folder, prompt, or automatic overwrite.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How the download attribute behaves

Source Can the anchor download it? Important constraint
HTML string converted to a blob: URL Yes Revoke the object URL only after the browser has started using it.
data: URL Yes Large documents can make data URLs inefficient; a Blob is usually clearer.
Same-origin URL Usually The server’s response headers and browser settings can affect handling.
Cross-origin URL used directly in an anchor Not as a general override The download attribute is not a way to defeat cross-origin restrictions.

The attribute is a filename suggestion. A Content-Disposition response header can influence the final filename or whether content is displayed. Browsers can also sanitize illegal filename characters or apply their own download policy.

Object-URL cleanup and memory

Each call to URL.createObjectURL() holds a reference to Blob data until it is released or the document goes away. Revoke URLs that you no longer need:

const objectUrl = URL.createObjectURL(blob);
try {
  link.href = objectUrl;
  link.click();
} finally {
  setTimeout(() => URL.revokeObjectURL(objectUrl), 30_000);
}

Immediate revocation can make the URL unavailable before the download begins. The 30-second delay is a practical cleanup interval, not a universal browser-mandated value; adjust it if your application downloads very large files or targets unusual environments.

When you need an existing file or in-place editing

The anchor pattern creates a new downloaded copy. It does not grant JavaScript permission to select an arbitrary path, overwrite an existing file, or silently edit a file on disk. A File System Access API workflow can let a user pick a file and approve writes where that API is supported, but browser support and permission requirements vary. Verify support in the browsers you target and provide the Blob download as a fallback.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Troubleshooting common failures

“Failed to fetch” or a CORS error

Cause: the destination is cross-origin and has not authorized your page, or the request was blocked by the network.

Fix: use a same-origin endpoint, configure the destination’s CORS response headers, or retrieve the page on a server you control and return it to the browser. A client-side proxy cannot grant permission that the destination withholds.

The file contains an error page instead of the target page

Cause: the server returned an HTTP error or an application login page. Always inspect response.status, response.url, and (when useful) the returned text before saving.

const response = await fetch(url, { redirect: "follow" });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
console.log("Final URL:", response.url);
const html = await response.text();

The downloaded file is empty or incomplete

Cause: the Blob was built from an empty variable, the response was consumed once already, or the object URL was revoked too early.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers

Fix: check html.length, call text() only once per response, and delay revokeObjectURL() until after the click has had time to start.

The page looks different when opened locally

Cause: relative links, blocked local scripts, missing assets, or server-side rendering that occurred only on the original request.

Fix: use absolute asset URLs where appropriate, inline required resources, or keep the page on a web server. Downloading response HTML does not reproduce the browser’s rendered pixels or JavaScript state.

The suggested filename is ignored

Cause: browser policy, an HTTP Content-Disposition header, or filename sanitization.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fix: provide a simple filename such as page.html, test in the target browser, and treat the value as a suggestion rather than a guarantee.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your real goal is a clean capture of a live page—not just its raw HTML—ScreenshotNeo makes one API request and returns PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

See the full parameter list in the ScreenshotNeo API documentation. A direct request looks like this:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo includes full-page captures with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper and page-range controls, custom CSS and JavaScript, clicks, waits, ad/tracker/request blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. Existing parameter names used by other screenshot APIs also work, which can simplify migration.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing provides two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to get started.

Frequently Asked Questions

Can JavaScript save HTML directly to a user-selected path?

Not with the ordinary anchor download flow. The browser controls the destination and any prompt. A File System Access API workflow can request user-approved file access where supported, but it is not universal.

Does downloading HTML also save images, CSS, and JavaScript?

No. The downloaded file contains the markup you supplied or fetched. Referenced assets remain separate resources unless you inline them or package them yourself.

Why does fetched HTML differ from what I see in DevTools?

The response may be server HTML before client-side JavaScript runs, while DevTools shows a live DOM after scripts and user interactions. Serialize the DOM for the latter, or use a browser-rendering capture service for a rendered result.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I download HTML from any public website in browser JavaScript?

No. Cross-origin Fetch requires the destination’s CORS permission, and a direct cross-origin anchor is not a universal bypass.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.