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

To draw a div reliably, wait for its images and fonts, choose an explicit image timeout, enable CORS only when the image server permits it, size the render to the element’s scroll dimensions, and await html2canvas(). The promise resolves to a canvas; failures usually come from unfinished or cross-origin resources rather than from the div itself.

The reliable capture pattern

Install html2canvas from npm with npm install html2canvas, then import it in your browser bundle. The target element must already be in the document and styled in the state you want to capture.

import html2canvas from 'html2canvas';

const element = document.querySelector('#capture');
if (!element) throw new Error('Missing #capture element');

const canvas = await html2canvas(element, {
  imageTimeout: 30000,
  useCORS: true,
  windowWidth: element.scrollWidth,
  windowHeight: element.scrollHeight
});

document.querySelector('#output').replaceChildren(canvas);

The documented imageTimeout default is 15,000 milliseconds. A value of 0 disables the timeout, but that can wait forever when a resource never resolves; use it for diagnosis or a deliberate policy, not as a universal fix.

Wait for every asset that affects the result

Images

Calling html2canvas immediately after inserting markup races the browser’s image loader. Wait for each image to finish, and distinguish a successful load from a broken URL with naturalWidth. The helper below also handles images that were already complete before the listener was attached.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
async function waitForImages(root) {
  const images = [...root.querySelectorAll('img')];

  await Promise.all(images.map(async (img) => {
    if (!img.complete) {
      await new Promise((resolve) => {
        const done = () => {
          img.removeEventListener('load', done);
          img.removeEventListener('error', done);
          resolve();
        };
        img.addEventListener('load', done, { once: true });
        img.addEventListener('error', done, { once: true });
      });
    }

    if (img.decode) {
      try { await img.decode(); } catch (_) { /* keep the failed image diagnosable */ }
    }

    if (!img.complete || img.naturalWidth === 0) {
      console.warn('Image did not load:', img.currentSrc || img.src);
    }
  }));
}

Fonts and transient UI

Web fonts can change line wrapping and therefore the canvas dimensions. Wait for document.fonts.ready when it exists. Pause carousels, CSS animations, blinking cursors, and expanding menus before capture; otherwise you may record an intermediate frame even though no timeout occurs.

await waitForImages(element);
if (document.fonts?.ready) await document.fonts.ready;

// Example: freeze application-specific animation state.
document.documentElement.classList.add('capture-mode');
try {
  const canvas = await html2canvas(element, {
    imageTimeout: 30000,
    useCORS: true,
    windowWidth: element.scrollWidth,
    windowHeight: element.scrollHeight
  });
  return canvas;
} finally {
  document.documentElement.classList.remove('capture-mode');
}

Use a CSS rule such as .capture-mode *, .capture-mode *::before, .capture-mode *::after { animation: none !important; transition: none !important; } if your application can safely freeze those effects.

Why html2canvas appears to hang

An image never finishes

The most common trigger is an image or other external resource that has not completed before the configured image timeout. Check the browser Network panel for stalled requests, redirects, authentication failures, mixed-content blocks, and URLs that return HTML instead of an image. Increase the finite timeout for genuinely slow but valid assets; do not hide a bad URL by setting an unlimited wait.

Cross-origin images

Set useCORS: true only when the image server returns a compatible Access-Control-Allow-Origin header. The option does not bypass browser security. If you control neither server headers nor the request path, fetch the asset through a same-origin proxy that you operate, then use that proxy URL in the page.

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

A canvas affected by an unreadable cross-origin image cannot be made readable later by html2canvas. Fix the resource policy before drawing, rather than trying to export a tainted canvas afterward.

Cross-origin iframes

html2canvas cannot read the contentDocument of a cross-origin iframe. You can capture content you own by rendering it in the same origin, or ask the embedded application for a separately generated image. No timeout value changes that browser boundary.

Choose timeout and rendering options deliberately

Option Use it for Important trade-off
imageTimeout: 15000 The documented default for image loading Fast failure for a resource that is genuinely unavailable; slow networks may need a larger value
imageTimeout: 30000 (or another finite value) Known slow but valid image hosts Each failed capture can occupy the page longer
imageTimeout: 0 Diagnostics or a policy that accepts indefinite waiting A request that never resolves can leave the capture waiting indefinitely
useCORS: true Remote images whose server sends compatible CORS headers Has no effect when the server omits the header; use a same-origin proxy instead
windowWidth/windowHeight Reproducing the layout viewport used during rendering Visible dimensions can clip a tall target; scroll dimensions do more work
scale Controlling output pixel density Higher values are sharper but consume more memory and processing time; the default follows window.devicePixelRatio

Capture a full-height div instead of a clipped viewport

For a vertically scrollable target, pass its scrollWidth and scrollHeight. This gives html2canvas a render window large enough for the complete element.

const element = document.querySelector('#capture');
const canvas = await html2canvas(element, {
  windowWidth: element.scrollWidth,
  windowHeight: element.scrollHeight,
  imageTimeout: 30000,
  useCORS: true
});

If the element is inside a horizontally constrained layout, verify that its scroll dimensions represent the intended output. A child with an intentionally clipped overflow region may still need a different capture target or a temporary capture-only style.

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

Reduce work before increasing timeouts

  • Capture the target element instead of document.body whenever possible.
  • Use x, y, width, and height to crop a known region.
  • Mark controls and overlays with data-html2canvas-ignore, or supply an ignoreElements predicate.
  • For a viewport-sized capture of a very large page, consider cullOffscreen so off-screen content is not rendered.
  • Choose the lowest scale that meets your output requirement. Pixel count, memory use, and encoding time rise quickly with scale.
const canvas = await html2canvas(element, {
  imageTimeout: 30000,
  useCORS: true,
  windowWidth: element.scrollWidth,
  windowHeight: element.scrollHeight,
  scale: Math.min(window.devicePixelRatio || 1, 2),
  ignoreElements: (node) => node.matches?.('[data-capture-ignore]')
});

The configuration documents removeContainer: true as the default cleanup behavior. In a long-lived application, release references to old canvases, avoid retaining large data URLs, and reuse or replace output nodes so repeated captures do not accumulate memory.

A reusable capture function with diagnostics

import html2canvas from 'html2canvas';

export async function captureDiv(selector, {
  timeout = 30000,
  scale = window.devicePixelRatio || 1
} = {}) {
  const element = document.querySelector(selector);
  if (!element) throw new Error(`No element matches ${selector}`);

  await waitForImages(element);
  if (document.fonts?.ready) await document.fonts.ready;

  const failedImages = [...element.querySelectorAll('img')]
    .filter(img => !img.complete || img.naturalWidth === 0)
    .map(img => img.currentSrc || img.src);
  if (failedImages.length) {
    throw new Error(`Images failed to load: ${failedImages.join(', ')}`);
  }

  const canvas = await html2canvas(element, {
    imageTimeout: timeout,
    useCORS: true,
    windowWidth: element.scrollWidth,
    windowHeight: element.scrollHeight,
    scale
  });

  return canvas;
}

// Example use:
const canvas = await captureDiv('#capture', { timeout: 30000, scale: 1 });
canvas.toBlob(blob => {
  if (!blob) throw new Error('Canvas encoding failed');
  const link = document.createElement('a');
  link.href = URL.createObjectURL(blob);
  link.download = 'capture.png';
  link.click();
  URL.revokeObjectURL(link.href);
}, 'image/png');

The explicit checks turn a vague timeout into a URL you can repair. Keep the timeout finite in production, log the target and resource policy, and retry only after correcting a transient network failure.

Troubleshooting checklist

Symptom Likely cause Fix
Promise waits until the timeout An image request is stalled or never resolves Inspect the Network panel, verify the URL and response, wait for images before capture, and use a larger finite timeout only for valid slow assets
Remote images are missing The server does not send compatible CORS headers Enable useCORS only when headers are present; otherwise serve through a same-origin proxy
Export throws a security or tainted-canvas error A cross-origin image was drawn without readable CORS permission Fix CORS or proxy the image before drawing; an already-tainted canvas cannot be repaired afterward
A tall div is cut off Capture used the visible viewport dimensions Set windowWidth: element.scrollWidth and windowHeight: element.scrollHeight
Text wraps differently or is missing Web fonts were still loading Await document.fonts.ready and capture after layout settles
Buttons, chat, or overlays appear in output Those nodes are part of the target subtree Use data-html2canvas-ignore, ignoreElements, or a capture-only CSS state
Browser tab becomes unresponsive Target and scale create too many pixels Capture a smaller element or crop, lower scale, and avoid rendering unnecessary off-screen content
Setting imageTimeout: 0 changes nothing The issue is not an image timeout, or a resource truly never resolves Check iframe origin, CORS, failed URLs, and layout state; restore a finite timeout while debugging

What html2canvas can and cannot reproduce

html2canvas runs in the browser and reconstructs a representation from the DOM and computed styles; it is not a native browser screenshot. The result can therefore differ from what a user sees in browser chrome or from pixels produced by a browser automation screenshot.

  • Same-origin DOM and styles can be rendered after assets are ready.
  • Cross-origin iframe documents cannot be read.
  • Cross-origin images require compatible CORS headers or a same-origin proxy.
  • Animations and late layout changes must be frozen or awaited if deterministic output matters.
  • Large dimensions and high scale increase memory pressure even when every resource loads correctly.
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 provides a website screenshot API and MCP server when you need a rendered page rather than a client-side canvas. One GET request returns PNG, JPEG, WebP, or a PDF; the API accepts a URL and handles the browser session for you.

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://stripe.com -o shot.webp

In 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)

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

See the complete parameter list and response details in the ScreenshotNeo documentation. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account to try the API without a card.

Practical decision guide

  • Use html2canvas when the content is already in your page, you need a canvas in the browser, and you can control image origins and UI timing.
  • Use a same-origin proxy when remote images are valid but their servers do not provide the CORS header you need.
  • Use a finite timeout for production reliability; reserve zero only for a consciously indefinite wait.
  • Use scroll dimensions for full-height elements, then reduce scale or crop if memory becomes the limiting factor.
  • Use ScreenshotNeo when you want a service or MCP client to load a URL, clean common overlays, and return an image or PDF without maintaining browser-capture code.

Frequently Asked Questions

Can html2canvas read a page inside a third-party iframe?

No. A cross-origin iframe’s document is inaccessible to browser JavaScript, so html2canvas cannot render it. Capture content from the owning origin or obtain an image from the embedded application.

When is imageTimeout: 0 a sensible setting?

Use it temporarily to diagnose whether a slow resource is the trigger, or when your application intentionally accepts an unbounded wait. For user-facing captures, a finite value exposes broken resources instead of hanging indefinitely.

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

Why can a capture succeed but still look different from the browser?

html2canvas rebuilds pixels from DOM and computed styles rather than taking a native browser screenshot. Font timing, animations, unsupported cross-origin content, and viewport dimensions can therefore change the result.

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.