ScreenshotNeo

BlogHow-to

How to Download an Image from an HTML Link

Learn the correct HTML, server, and JavaScript methods for downloading same-origin and cross-origin images, with troubleshooting and runnable examples.

By the ScreenshotNeo team1 October 20266 min read

For an image hosted on the same origin as your page, put its URL in an anchor and add the download attribute:

<a href="images/photo.jpg" download="photo.jpg">Download image</a>

The value of download is a suggested filename. The browser can change it to satisfy filesystem rules, and browser settings determine whether the file is saved automatically, prompts the user, or opens inline. The attribute is designed for same-origin URLs and also works with blob: and data: URLs. It does not reliably force an arbitrary cross-origin URL to download.

1. Use the download attribute for a same-origin image

Two URLs have the same origin only when their scheme, host, and port all match. If your page is https://example.com/gallery, an image at https://example.com/images/photo.jpg is same-origin. A file at https://cdn.example.net/photo.jpg is not.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Download image</title>
</head>
<body>
  <a href="/images/photo.jpg" download="photo.jpg">
    Download image
  </a>
</body>
</html>

Omit the filename when you do not need to suggest one:

<a href="/images/photo.jpg" download>Download image</a>

Without a value, the browser may derive a name from the response’s Content-Disposition header, the final URL path segment, or the media type. MDN documents the download attribute as causing the browser to treat the linked URL as a download. MDN: the <a> element

2. Make the server return an attachment

If you control the image server, the most reliable server-side signal is the Content-Disposition response header:

HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Disposition: attachment; filename="photo.jpg"

[binary image bytes]

attachment tells the browser to treat the response as a download. The filename parameter suggests a name. For non-ASCII names, servers can also send filename* with an encoded UTF-8 value. When both a header filename and an HTML download filename exist, browser behavior can give the header priority.

Use the header when the file is cross-origin, when links are generated by an API, or when every client should receive attachment semantics. It still cannot control the user’s save location or guarantee an identical prompt in every browser.

3. Download a cross-origin image with JavaScript and a Blob

A page script cannot read an image response from another origin unless that server grants access with CORS response headers. Adding crossorigin to markup does not grant permission by itself.

If the remote server allows your origin, fetch the bytes, create a temporary Blob URL, click a download link, and revoke the URL:

<button id="download" type="button">Download remote image</button>
<script>
const button = document.querySelector('#download');

button.addEventListener('click', async () => {
  const imageUrl = 'https://cdn.example.com/photo.jpg';

  try {
    const response = await fetch(imageUrl, { mode: 'cors' });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);

    const blob = await response.blob();
    const blobUrl = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = blobUrl;
    link.download = 'photo.jpg';
    document.body.appendChild(link);
    link.click();
    link.remove();
    URL.revokeObjectURL(blobUrl);
  } catch (error) {
    console.error('Image download failed:', error);
  }
});
</script>

The remote server must return an appropriate Access-Control-Allow-Origin value. If it does not, the browser blocks script access; a page-only change cannot bypass that policy. MDN: Cross-Origin Resource Sharing (CORS)

When a Blob download is appropriate

  • You need to transform, inspect, or combine the image before saving.
  • You need to choose a filename in client code.
  • The server permits CORS and you want one consistent click flow.

Do not use this pattern merely to work around a server that denies cross-origin access. Proxy the image through a server you control only when you have permission to do so and can handle the associated security, caching, and bandwidth responsibilities.

4. Handle responsive images deliberately

An <img> can select different files from srcset and sizes based on viewport width and pixel density. The URL in src may not be the file currently displayed.

<img
  src="/images/photo-800.jpg"
  srcset="/images/photo-800.jpg 800w, /images/photo-1600.jpg 1600w"
  sizes="(max-width: 600px) 100vw, 600px"
  alt="Mountain landscape"
>

<a href="/images/photo-1600.jpg" download="mountain-large.jpg">
  Download the 1600px image
</a>

If the user should download exactly the displayed candidate, decide which URL to use in script:

const image = document.querySelector('img');
const displayedUrl = image.currentSrc || image.src;
const link = document.createElement('a');
link.href = displayedUrl;
link.download = 'displayed-image';
link.click();

For a guaranteed resolution, expose explicit download links rather than guessing which candidate the browser selected. MDN: Responsive images

5. Accessibility and interaction details

  • Use link text that describes the action, such as “Download product photo,” instead of “Click here.”
  • Keep the image’s alt text meaningful for users who are viewing it, but do not rely on alt text as the filename.
  • Use a button for an operation that performs JavaScript processing; use an anchor for a direct resource link.
  • Tell users when a large file will be downloaded if size or bandwidth matters.

6. Troubleshooting

Symptom Likely cause Fix
The image opens in a tab The URL is cross-origin, or the browser is not honoring the attribute Configure Content-Disposition: attachment on the server, or use a CORS-permitted Blob flow
Fetch fails with a CORS error The image server does not allow your origin Change the server’s CORS policy or download through an authorized backend; markup alone cannot grant access
The saved file has the wrong name The response header, URL, or browser sanitized the suggestion Inspect Content-Disposition, provide a safe download value, and avoid filesystem-reserved characters
The downloaded image is the wrong size srcset selected another candidate Link the desired source explicitly or use currentSrc when the displayed candidate is intended
The link returns HTML instead of an image The URL redirects to a login page, error page, or bot check Inspect the final response and Content-Type; require an authenticated or direct image endpoint
The Blob URL stops working It was revoked before the browser started the download Revoke it after triggering the link, and keep the code inside the click handler
Nothing happens on mobile Browser download UI and settings differ Use a normal anchor as a fallback and provide clear visible feedback

7. Performance, reliability, and cost

  • A direct anchor usually has the least JavaScript and memory overhead because the browser streams the response itself.
  • Blob downloads buffer the response in browser memory. Large images can increase memory use and may fail on constrained devices.
  • Server-side attachment headers work for every client that can reach the server, but redirects, authentication, and expiring URLs still affect reliability.
  • For repeated downloads, use cache headers and stable image URLs where appropriate. Avoid downloading a larger responsive candidate than the user needs.
  • Check the final status code and Content-Type before treating a response as an image. A successful HTTP status does not prove that the body is an image.

8. Or skip the browser setup

If your real task is obtaining a clean image of a web page or rendered element, ScreenshotNeo provides a screenshot API instead of requiring browser automation:

API documentation

cURL

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)
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}`);

ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and billing result. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account.

9. FAQ

Does download always force a save dialog?

No. The browser and user settings decide whether to prompt, save automatically, or handle the resource another way.

Can I force a download from any external image URL?

No. For cross-origin URLs, use a server response with Content-Disposition: attachment or a CORS-permitted fetch.

Should I use a Blob for every image?

No. Use a direct anchor for a simple same-origin download. Use a Blob when you need JavaScript processing or a client-generated filename.

Why does my download contain a login page?

The URL likely redirected to authentication or an error page. Inspect redirects, status, and Content-Type before saving the response.