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

To capture S3-hosted Leaflet tiles with html2canvas, three layers must agree: the S3 (or CDN) response must allow your page’s exact origin, Leaflet must request tiles with its crossOrigin option, and html2canvas must run with useCORS: true. Verify one real tile response in browser developer tools before changing code. If the image host cannot return acceptable CORS headers, use a controlled proxy or omit that layer; allowTaint cannot make a tainted canvas exportable.

Why the map disappears or the canvas cannot be exported

A page, its JavaScript, and an S3 tile URL are compared by origin: scheme, host, and port. A tile is cross-origin when any of those differs. Browsers permit an image to be displayed in many situations, but drawing an image without an approved CORS response into a canvas taints that canvas. Reading pixels, calling toDataURL(), or creating a blob is then blocked by the browser. html2canvas documents this as a browser security restriction, not a library switch it can bypass (html2canvas FAQ; MDN: CORS enabled images).

The configuration is therefore a chain:

  • The browser sends the tile request in CORS mode.
  • The tile endpoint responds with an Access-Control-Allow-Origin value matching the page origin.
  • The object is still readable under S3, CDN, and bucket-policy rules.
  • html2canvas attempts CORS image loading.

Setting allowTaint: true only permits html2canvas to draw images that would taint the canvas; it does not grant permission to read or export the resulting pixels (html2canvas FAQ).

Start with the actual failing tile

  1. Open the map page and its browser developer tools.
  2. In Network, filter for one tile image and reproduce the capture.
  3. Record the final request URL, scheme, host, port, status, request Origin, and response headers.
  4. Inspect the response from the host the browser actually contacted. If a CDN or custom domain is in front of S3, inspect that endpoint rather than only the bucket endpoint.

The request origin must match an S3 AllowedOrigins entry. A rule for https://www.example.com does not automatically match https://example.com, another port, or a local-development origin. AWS describes the matching elements and rule processing in its S3 CORS documentation.

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

Configure CORS on the serving endpoint

In the S3 bucket’s CORS configuration, allow the exact origin used by the map page and the method used for tiles (normally GET). Keep request headers as narrow as your application permits. This illustrative document allows all request headers because some tile deployments add headers; replace the origin and remove unnecessary entries:

[
  {
    "AllowedOrigins": ["https://maps.example.com"],
    "AllowedMethods": ["GET"],
    "AllowedHeaders": ["*"]
  }
]

This is a configuration shape, not a guarantee for a particular bucket. AWS documents the console and API formats in Using cross-origin resource sharing (CORS) and provides examples at Enabling cross-origin resource sharing.

S3 CORS controls whether a browser may share the response with your page; it does not make a private object public and does not replace IAM, ACL, bucket-policy, or CDN authorization. AWS explicitly notes that those permissions continue to apply (AWS S3 CORS). After changing the rule, check the response again from the real tile hostname. A CDN may cache an older response or fail to forward the relevant request and response headers, so a correct bucket rule can still produce an unusable browser response.

Tell Leaflet to request tiles with CORS

Set the TileLayer’s documented crossOrigin option when creating the layer. Use the value supported by the Leaflet version installed in your application and by the tile provider. The common anonymous form is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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
const tiles = L.tileLayer(
  'https://tiles.example.com/{z}/{x}/{y}.png',
  { crossOrigin: 'anonymous' }
).addTo(map);

Leaflet’s current API reference documents the TileLayer option and its accepted values; check the reference that matches your installed release (Leaflet API reference). If you create the layer without this option, the browser may load a visually usable image without making it safe for canvas use.

Enable CORS loading in html2canvas

Pass useCORS: true when capturing the map element. html2canvas documents this option as an attempt to load images using CORS, and its default is false (html2canvas configuration).

const mapElement = document.getElementById('map');
const canvas = await html2canvas(mapElement, {
  useCORS: true
});

canvas.toBlob((blob) => {
  if (!blob) throw new Error('The canvas could not be encoded');
  const link = document.createElement('a');
  link.href = URL.createObjectURL(blob);
  link.download = 'leaflet-map.png';
  link.click();
  URL.revokeObjectURL(link.href);
}, 'image/png');

Capture only after the map has finished adding its tiles. A fixed delay is simple but not deterministic; a production page should wait for its tile layer’s load events or otherwise verify that the required tile images are complete before calling html2canvas. This prevents a timing problem from being mistaken for CORS.

Complete browser example

The following example shows the order: create the map, configure the tile layer, wait for the map to settle, then capture. Replace the tile URL and element ID with your application’s values and validate the options against the versions you install.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import L from 'leaflet';
import html2canvas from 'html2canvas';

const map = L.map('map').setView([51.505, -0.09], 13);
const tiles = L.tileLayer(
  'https://tiles.example.com/{z}/{x}/{y}.png',
  {
    crossOrigin: 'anonymous',
    attribution: 'Map data'
  }
).addTo(map);

function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

map.whenReady(async () => {
  // Use your own tile-load condition when available.
  await wait(1000);

  const element = document.getElementById('map');
  const canvas = await html2canvas(element, {
    useCORS: true,
    backgroundColor: null
  });

  const blob = await new Promise(resolve =>
    canvas.toBlob(resolve, 'image/png')
  );
  if (!blob) throw new Error('No image blob was produced');

  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'map.png';
  a.click();
  URL.revokeObjectURL(url);
});

If a tile is still loading when the capture starts, the output may contain gaps even when CORS is correct. Conversely, seeing tiles in the DOM does not prove that their responses include the headers needed for canvas access.

When the headers look right but capture still fails

Object or bucket authorization blocks the request

A CORS rule cannot authorize a request that S3 or a CDN rejects. Check the tile’s status code, object existence, and the policies used by the serving endpoint. A successful CORS preflight or response header does not override those controls (AWS S3 CORS).

The CDN or custom domain changes the response

Repeat the inspection against the final hostname in the tile request. Confirm that the CDN forwards the request’s Origin and returns the appropriate CORS response on cache hits as well as misses. The required behavior is determined by the endpoint the browser contacts, not by the bucket configuration in isolation.

The tile provider cannot opt in to your origin

You cannot repair a third-party image response from JavaScript. html2canvas documents a proxy as an alternative (html2canvas proxy documentation). A proxy should fetch only permitted image URLs, enforce authentication and size/time limits, validate content, and avoid becoming an unrestricted public fetch service. It must return the image from an origin your page can use.

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

The imagery is optional

If the export remains useful without satellite or base-map tiles, exclude those elements from the capture. html2canvas supports ignoring elements through its configuration (html2canvas configuration). Keep overlays such as markers or labels if they are the part of the map the user needs.

Separate CORS from visual fidelity and size limits

Fixing CORS only makes the pixels readable. html2canvas reconstructs a page from DOM and style information rather than taking a native browser screenshot, and its documentation lists CSS support limitations (html2canvas documentation). Leaflet panes, transforms, fonts, blend modes, controls, and custom overlays can therefore look different in the export even when every tile loads.

Large maps can also exceed browser or device canvas dimensions. If a full-page or high-resolution export fails, reduce the capture element’s dimensions or scale, capture regions separately, and test on the browsers and devices you support. Treat a blank or partial result as a separate rendering or resource-limit investigation after the network response is confirmed.

Choose the remedy that fits the deployment

Option Use it when Checks before shipping
S3/CDN CORS plus Leaflet crossOrigin and html2canvas useCORS You control the tile endpoint. Exact origin, GET, required headers, object permissions, CDN forwarding and cache behavior.
Application-controlled proxy The remote service cannot return a suitable CORS response. URL allow-listing, authentication, content validation, limits, latency and response headers.
Exclude the layer Base imagery is optional for the export. Whether the remaining overlays still communicate the intended result.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One request can return a PNG, JPEG, WebP, or PDF of a URL, so the capture runs outside your page’s canvas and tile-loading code. See the ScreenshotNeo documentation for request options.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://your-site.example/map -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://your-site.example/map"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://your-site.example/map' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Before capture, ScreenshotNeo accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

The Free plan includes 1,000 shots per month with no card. Paid plans are Starter $5 for 3,000 shots, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free, and every feature is included on every plan. Create a free ScreenshotNeo account.

Troubleshooting checklist

  • No Access-Control-Allow-Origin header: add the exact page origin to the CORS rule on the endpoint serving the tile, then verify the final CDN or custom-domain response.
  • Header names an unexpected origin: correct the rule or deployment so the response matches the request’s Origin.
  • Tile status is denied or missing: fix object, bucket, CDN, or authentication permissions; CORS is not authorization.
  • Tiles display but export throws a security error: confirm Leaflet’s crossOrigin option was set when the layer was created and that html2canvas receives useCORS: true.
  • Only some tiles are absent: inspect those individual URLs for different hosts, redirects, status codes, or cached headers.
  • Network is clean but appearance is wrong: investigate html2canvas CSS support, map timing, overlay structure, and canvas-size limits separately.
  • Third-party host will not provide CORS: use a secured proxy or omit the affected layer; client-side flags cannot override the host’s response.

Frequently Asked Questions

Does adding an S3 CORS rule make a private tile public?

No. CORS controls browser response sharing; S3 and CDN authorization policies still decide whether the object can be fetched.

Why do tiles render on screen but disappear from the exported image?

Displaying an image and reading it through canvas are different permissions. Without a matching CORS response and a CORS-mode request, drawing the tile taints the canvas.

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

Can I fix an uncooperative tile server with allowTaint: true?

No. That option does not make a tainted canvas readable. Use a server you control, a secured proxy, or exclude the layer.

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.