How to Replace and Remove Previous html2canvas Canvases
Replace html2canvas output safely, remove only the canvas you own, prevent stale renders, reuse nodes, and fix CORS and cleanup issues.
Direct answer: html2canvas(element, options) returns a Promise that resolves to an HTMLCanvasElement. Your code decides where that canvas is inserted, so your code must remove or replace the previous output. Keep a reference to the old canvas, scope cleanup to a dedicated host or marker, await the next render, and guard against stale asynchronous completions.
The library’s removeContainer option cleans temporary cloned DOM nodes created during rendering. It does not remove a canvas that your application appended to the document. See the official getting-started documentation and configuration reference.
1. Replace the previous canvas in a dedicated host
A dedicated host is the safest default because it cannot remove charts, signatures, games, or other canvases elsewhere on the page.
<button id="render" type="button">Render preview</button>
<section id="source">
<h1>Invoice preview</h1>
<p>This element is captured by html2canvas.</p>
</section>
<div id="preview" aria-live="polite"></div>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<script>
const source = document.querySelector('#source');
const host = document.querySelector('#preview');
const button = document.querySelector('#render');
let previousCanvas = null;
let renderSerial = 0;
async function replacePreview() {
const serial = ++renderSerial;
button.disabled = true;
try {
const nextCanvas = await html2canvas(source, {
removeContainer: true
});
// A newer request may have started while this one was rendering.
if (serial !== renderSerial) return;
if (previousCanvas?.isConnected) {
previousCanvas.remove();
}
host.append(nextCanvas);
previousCanvas = nextCanvas;
} finally {
if (serial === renderSerial) button.disabled = false;
}
}
button.addEventListener('click', replacePreview);
</script>
The reference makes removal deterministic. isConnected prevents errors if another part of your application already detached the node. The serial guard prevents an older Promise from replacing a newer result.
2. Mark generated canvases when the host is shared
If several components render into one container, mark only the canvases owned by this feature. Do not use document.querySelectorAll('canvas') across the whole document.
async function replaceMarkedPreview(source, host) {
host.querySelector('canvas[data-html2canvas-output]')?.remove();
const nextCanvas = await html2canvas(source, {
removeContainer: true
});
nextCanvas.dataset.html2canvasOutput = 'true';
host.append(nextCanvas);
return nextCanvas;
}
A class works too:
host.querySelector('canvas.html2canvas-output')?.remove();
const next = await html2canvas(source);
next.classList.add('html2canvas-output');
host.append(next);
3. Reuse an existing canvas node
The configuration includes a canvas option: an existing canvas element can be supplied as the drawing target. This is useful when other code stores the node, observes it, or depends on stable DOM identity.
const source = document.querySelector('#source');
const output = document.querySelector('#previewCanvas');
await html2canvas(source, {
canvas: output,
removeContainer: true
});
With this approach, html2canvas draws into the application-owned node. Confirm the dimensions and context behavior required by the rest of your code before switching from a newly returned canvas.
4. Prevent older renders from winning
html2canvas is asynchronous and documents a Promise result, but it does not document cancellation. If a user changes settings quickly, two renders can finish out of order. Either serialize requests or ignore stale completions.
Serialize captures
let renderQueue = Promise.resolve();
function queuePreview(source, host) {
renderQueue = renderQueue.then(async () => {
const next = await html2canvas(source);
host.querySelector('canvas[data-html2canvas-output]')?.remove();
next.dataset.html2canvasOutput = 'true';
host.append(next);
});
return renderQueue;
}
Keep only the newest result
let newestRequest = 0;
async function renderNewest(source, host) {
const requestId = ++newestRequest;
const next = await html2canvas(source);
if (requestId !== newestRequest) {
// This result is stale. It was never inserted, so nothing needs removing.
return;
}
host.querySelector('canvas[data-html2canvas-output]')?.remove();
next.dataset.html2canvasOutput = 'true';
host.append(next);
}
5. Understand removeContainer
removeContainer defaults to true. It controls cleanup of the cloned DOM container that html2canvas creates temporarily while rendering. It does not know which canvas your application appended, and it does not remove that output node. Leave it enabled unless you have a specific reason to inspect the temporary clone.
const canvas = await html2canvas(source, {
removeContainer: true
});
// Application-owned output cleanup:
oldCanvas?.remove();
host.append(canvas);
6. Complete replacement helper
This helper handles a missing host, stale results, replacement, and optional html2canvas settings.
export function createCanvasReplacer({ source, host, options = {} }) {
let previousCanvas = null;
let serial = 0;
return async function render() {
const current = ++serial;
const nextCanvas = await html2canvas(source, {
removeContainer: true,
...options
});
if (current !== serial) return null;
if (!host?.isConnected) {
throw new Error('The canvas host is no longer connected');
}
previousCanvas?.remove();
nextCanvas.dataset.html2canvasOutput = 'true';
host.replaceChildren(nextCanvas);
previousCanvas = nextCanvas;
return nextCanvas;
};
}
const render = createCanvasReplacer({
source: document.querySelector('#source'),
host: document.querySelector('#preview'),
options: { scale: 2 }
});
render();
7. Rendering options that affect replacement
| Option | Use | Replacement implication |
|---|---|---|
canvas |
Draw into an existing canvas | Preserves node identity; you still own its lifecycle |
removeContainer |
Remove temporary cloned DOM | Does not remove your appended output canvas |
useCORS |
Request cross-origin images with CORS | Helps keep the resulting bitmap readable when the server permits it |
proxy |
Route image loading through a proxy | Useful when assets cannot be requested directly with CORS |
allowTaint |
Allow images that taint the canvas | The canvas may render but become unreadable for export or pixel access |
scale |
Control output pixel density | Higher values increase memory, render time, and replacement frequency cost |
backgroundColor |
Set the output background | Use a stable color when transparent output is not desired |
These controls are documented in the official configuration reference. html2canvas reconstructs a page from DOM and styles; it is not a native, pixel-perfect browser screenshot engine.
8. Cross-origin images and unreadable canvases
A canvas can display an image and still be blocked when your code later calls toDataURL(), toBlob(), or reads pixels. Cross-origin images can taint the bitmap under browser security rules.
const canvas = await html2canvas(source, {
useCORS: true,
proxy: 'https://your-approved-image-proxy.example/canvas',
allowTaint: false
});
canvas.toBlob(blob => {
if (!blob) {
console.error('Canvas could not be exported; check image CORS and proxy settings.');
return;
}
// upload or download blob
}, 'image/png');
Use useCORS only when the image server sends an appropriate CORS header. A proxy must be controlled and configured for your application. Setting allowTaint: true can permit rendering but is unsuitable when you need a readable export.
9. Common errors and fixes
| Symptom | Cause | Fix |
|---|---|---|
| Every click adds another canvas | The new result is appended without removing the old one | Keep a reference, use replaceChildren, or remove a marked output first |
| Charts or signatures disappear | A broad selector removed unrelated canvases | Use a dedicated host or data-html2canvas-output marker |
removeContainer did not remove the visible canvas |
It cleans temporary clones, not application output | Remove the appended node yourself |
| Old content replaces newer content | Promises completed out of order | Serialize requests or add a request serial guard |
SecurityError during export |
A cross-origin image tainted the canvas | Use same-origin assets, useCORS with server permission, or a proxy; avoid allowTaint when exporting |
| Nothing is inserted | The host selector returned null or the host was unmounted |
Validate selectors before capture and check host.isConnected |
| Output is blurry or huge | Scale, device pixel ratio, or source dimensions are high | Set an intentional scale, limit capture size, and release discarded canvases |
| Memory grows after many renders | Old canvases remain referenced or attached | Detach old nodes, clear application references, and avoid retaining large data URLs |
10. Performance and reliability checklist
- Capture only the element you need instead of a large document.
- Use a dedicated output host and replace in one DOM operation.
- Serialize captures when every result matters; otherwise ignore stale results.
- Choose a deliberate
scale; higher resolution consumes more memory. - Prefer
toBlob()over large base64 data URLs for uploads. - Wait until fonts, images, and dynamic content are ready before calling html2canvas.
- Handle a component unmounting while a Promise is pending.
- Keep CORS and proxy behavior consistent across every image origin.
- Do not rely on
removeContainerfor application cleanup.
11. Or skip the browser setup
If you need a URL screenshot rather than a canvas inside the current page, ScreenshotNeo provides a single GET request that returns PNG, JPEG, WebP, or PDF. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, 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 lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for all options.
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,
)
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(`Screenshot failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', bytes);
There is a free plan with 1,000 screenshots per month and no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan. Create a free ScreenshotNeo account.
12. FAQ
Does html2canvas replace the old canvas automatically?
No. It returns a new canvas unless you supply an existing canvas through the canvas option. Insertion and replacement are application code.
Can I remove the last canvas with replaceChildren()?
Yes, when the host contains only output owned by this feature. Use a marker or dedicated host if other children must remain.
Should I set removeContainer to false to keep the result?
No. The result canvas is separate from the temporary clone. Keeping the clone does not replace the need to manage your output node.
Why does a canvas look correct but fail to download?
Most often a cross-origin image tainted it. Configure CORS or a proxy before capture and keep allowTaint disabled when you need exportable pixels.
Is a browser screenshot API better for a server-side URL capture?
For a screenshot of a remote URL, an API removes browser setup and page-cleanup code. ScreenshotNeo also reports whether a response was billed and provides an MCP server for AI-agent workflows.


