How to Capture CSS WebKit Filters with html2canvas
Learn why html2canvas misses WebKit filters, how to diagnose it, practical workarounds, and when a real browser screenshot is the better choice.
Short answer: html2canvas does not take a pixel screenshot. It walks the DOM and paints its own canvas representation. A CSS filter that renders correctly in Safari or another WebKit browser can therefore be missing or different in the canvas because html2canvas must implement that property and its rendering details itself.
For a quick diagnosis, reduce the page to one filtered element, record the browser, operating system, html2canvas version, and exact CSS declaration, then compare a normal capture with a capture made after changing the cloned document in onclone. If the output must match browser pixels exactly, use a native browser screenshot workflow such as an extension API or server-side Puppeteer/Playwright. If you need a hosted API, ScreenshotNeo captures pages in a real browser and returns PNG, JPEG, WebP, or PDF.
What “WebKit filters” means
The filter property applies effects to an element’s rendered content. Safari’s older CSS Visual Effects documentation shows the prefixed form, -webkit-filter, with functions such as hue-rotate() and saturate(). Modern code normally includes the unprefixed declaration as well:
.photo {
-webkit-filter: hue-rotate(180deg) saturate(200%);
filter: hue-rotate(180deg) saturate(200%);
}
backdrop-filter is different: it applies an effect to content behind a translucent element. WebKit lists backdrop-filter and the -webkit-backdrop-filter alias as supported browser features. Browser-engine support does not guarantee that html2canvas can reproduce either property.
.glass {
background: rgb(255 255 255 / 35%);
-webkit-backdrop-filter: blur(16px);
backdrop-filter: blur(16px);
}
Keep these questions separate when debugging:
- Does the browser render the declaration correctly?
- Does html2canvas know how to reconstruct that declaration?
- Is the content behind a
backdrop-filterelement present in the captured DOM? - Are cross-origin images, canvases, or iframes changing what can be read?
Build a minimal reproduction
- Create a page with a solid background, one image or colored panel, and only the filter rule under investigation.
- Open it in the browser where the live page looks correct.
- Capture the same element with html2canvas.
- Write down the browser and version, operating system, html2canvas package version, exact declaration, and whether the effect is
filterorbackdrop-filter. - Test a capture with the filter removed or replaced by a pre-rendered equivalent.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>html2canvas filter test</title>
<style>
body { margin: 0; padding: 40px; background: #182033; color: white; }
.stage { width: 520px; padding: 32px; background: linear-gradient(135deg, #ff7a18, #af002d 70%); }
.filtered { width: 320px; height: 180px; object-fit: cover; filter: hue-rotate(150deg) saturate(180%); }
.glass { margin-top: 24px; padding: 24px; background: rgb(255 255 255 / 35%); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); }
</style>
</head>
<body>
<div id="stage" class="stage">
<div class="filtered" style="background: linear-gradient(90deg, #00d4ff, #ff00a8);"></div>
<div class="glass">Content behind this panel should remain visible.</div>
</div>
<button id="save">Capture</button>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<script>
document.querySelector('#save').addEventListener('click', async () => {
const canvas = await html2canvas(document.querySelector('#stage'), {
backgroundColor: null,
logging: true
});
const link = document.createElement('a');
link.download = 'filter-test.png';
link.href = canvas.toDataURL('image/png');
link.click();
});
</script>
</body>
</html>
This test separates a missing filter implementation from unrelated layout, timing, image-loading, or cross-origin problems.
Capture an element with html2canvas
import html2canvas from 'html2canvas';
const target = document.querySelector('#stage');
if (!target) throw new Error('Missing #stage');
const canvas = await html2canvas(target, {
backgroundColor: null,
scale: window.devicePixelRatio,
useCORS: true,
logging: true
});
document.body.appendChild(canvas);
The documented options most useful for this problem are:
| Option | Use | Limit |
|---|---|---|
onclone |
Inspect or modify the cloned document used for rendering. | It cannot add browser rendering support that html2canvas does not implement. |
foreignObjectRendering |
Experiment with the browser’s SVG foreignObject path. | It is not a fidelity guarantee and has browser and security constraints. |
useCORS |
Request eligible cross-origin images with CORS. | It does not implement CSS filters or bypass server headers. |
proxy |
Route eligible image requests through a proxy. | It cannot make cross-origin iframes readable or override content-security rules. |
scale |
Control output pixel density. | Higher values increase memory use and render time. |
backgroundColor |
Set a solid background or use null for transparency. |
Transparency does not restore missing effects. |
Use onclone to compare rendering paths
onclone runs after html2canvas clones the document and before it renders the clone. Use it to log computed styles, disable a filter for a control capture, or substitute a static fallback.
const canvas = await html2canvas(document.querySelector('#stage'), {
logging: true,
onclone: (clonedDocument) => {
const original = document.querySelector('#stage .filtered');
const clone = clonedDocument.querySelector('#stage .filtered');
console.table({
originalFilter: original ? getComputedStyle(original).filter : 'missing',
cloneFilter: clone ? getComputedStyle(clone).filter : 'missing',
originalBackdrop: original ? getComputedStyle(original).backdropFilter : 'missing',
cloneBackdrop: clone ? getComputedStyle(clone).backdropFilter : 'missing'
});
// Control experiment: remove effects only in the cloned document.
// clone?.style.setProperty('filter', 'none');
// clone?.style.setProperty('backdrop-filter', 'none');
// clone?.style.setProperty('-webkit-backdrop-filter', 'none');
}
});
If disabling the effect makes the rest of the capture correct, the filter is the likely unsupported or incomplete part. If the clone is missing the target or its background, fix DOM selection or capture bounds first.
Workarounds when a filter is missing
1. Capture a pre-rendered asset
For a stable design, render the filtered image ahead of time and capture the resulting PNG or WebP. This removes the need for html2canvas to reconstruct the effect.
2. Use a static fallback in the clone
const canvas = await html2canvas(target, {
onclone: (doc) => {
const element = doc.querySelector('.filtered');
if (element) {
element.style.filter = 'none';
element.style.backgroundImage = 'url("/assets/filtered-static.webp")';
}
}
});
3. Try foreignObjectRendering as an experiment
const canvas = await html2canvas(target, {
foreignObjectRendering: true,
logging: true
});
Compare this result with the default renderer. Treat a different result as diagnostic evidence, not proof that every WebKit filter is supported. Browser restrictions, SVG serialization, fonts, external images, and security policies can still change the output.
4. Switch to a real browser screenshot
When exact browser pixels matter, use a native screenshot facility in a browser extension or automate a real browser with Puppeteer or Playwright. These approaches render the page through the browser engine instead of rebuilding it from DOM and computed styles. They still require you to handle navigation timing, authentication, cross-origin policy, and resource failures.
Cross-origin and backdrop-filter edge cases
- Cross-origin images: An image must permit CORS for the browser to safely use it in a canvas.
useCORSonly requests that permission; the image server must send appropriate headers. - Canvas tainting: A cross-origin image drawn without permission can prevent reading the resulting canvas.
- Cross-origin iframes: html2canvas cannot inspect their contents. Same-origin iframe contents can be rendered recursively.
- Backdrop context: A backdrop blur needs visible pixels behind the translucent element. Capturing only the panel, or omitting its background, can make a correct blur appear empty.
- Content security policy: html2canvas cannot bypass browser content-security rules.
- Fonts and late resources: Wait for fonts and images before capture; otherwise layout and effect input may differ from the live page.
- Large pages: Very large dimensions or high
scalevalues can exhaust canvas memory.
await document.fonts.ready;
await Promise.all([...document.images].map((image) => {
if (image.complete) return Promise.resolve();
return new Promise((resolve) => {
image.addEventListener('load', resolve, { once: true });
image.addEventListener('error', resolve, { once: true });
});
}));
const canvas = await html2canvas(target, { scale: 1 });
Troubleshooting checklist
| Symptom | Likely cause | Fix |
|---|---|---|
filter works live but disappears in the image |
html2canvas does not implement that property or function completely. | Run the minimal reproduction, test onclone, use a pre-rendered fallback, or switch to a real browser screenshot. |
backdrop-filter: blur() has no visible effect |
No meaningful content is behind the translucent element in the captured DOM. | Capture the background and the panel together; verify opacity and stacking order. |
| Only some images are missing | Cross-origin response lacks usable CORS headers. | Serve images with CORS, use an eligible proxy, or host the assets on the same origin. |
| An iframe is blank | The iframe is cross-origin. | Capture it separately from a page that can access it, or use a real browser workflow. |
| Output is blurry | Low canvas scale or a resized display. | Increase scale carefully and inspect the actual canvas dimensions. |
| Capture is blank or clipped | Wrong target, hidden content, viewport bounds, or capture started before layout settled. | Check the selector, wait for fonts/images, ensure the element is visible, and log dimensions. |
| Console shows security errors | Canvas security policy, CSP, or cross-origin content. | Fix server headers or use browser automation; html2canvas cannot bypass those controls. |
| Different browsers produce different results | Browser APIs, CSS support, fonts, and html2canvas paths differ. | Record exact versions and test the target browser rather than assuming universal support. |
The html2canvas FAQ explains that every CSS property must be implemented manually and that full CSS support is not possible. If you have a reduced test case showing a missing property, provide it to the project as an issue.
Performance and reliability
- Capture the smallest element that satisfies the requirement instead of the entire document.
- Use
scale: 1for previews and raise it only for final assets. - Wait for layout, fonts, images, and any application data before starting.
- Disable animations and transitions in the clone when deterministic output matters.
- Keep filter inputs stable; animated blur, video, and canvas content can produce different frames.
- Retrying a failed capture will not fix an unsupported CSS property. First classify the failure as rendering support, resource access, timing, or memory.
- For repeatable server jobs, pin browser and library versions and keep a small visual regression fixture for each filter you depend on.
Or skip the browser setup
ScreenshotNeo provides a hosted real-browser screenshot API. It removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. It also offers an MCP server so Claude, Cursor, and other MCP clients can call take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for all options. A basic request is:
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 buffer = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', buffer);
ScreenshotNeo supports PNG, JPEG, WebP, and PDF, plus full-page capture, CSS-selector element capture, custom CSS and JavaScript, dark mode, device presets, retina scale, waits, request blocking, headers, cookies, authentication, timezone, geolocation, caching, signed links, asynchronous jobs, webhooks, bulk capture, and a usage API. It is useful when you need browser pixels without maintaining Puppeteer or Playwright. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
FAQ
Does html2canvas support -webkit-filter?
Browser support for the prefixed declaration is separate from html2canvas support. Test the exact filter, browser, and html2canvas version you use.
Why does a normal filter work but backdrop blur does not?
A normal filter changes the element’s own content. A backdrop filter needs pixels behind a translucent element, and those pixels must be present in the captured DOM.
Will useCORS make CSS filters work?
No. It can help eligible cross-origin images load, but it does not implement missing CSS properties.
Can html2canvas capture a cross-origin iframe?
No. Cross-origin iframe contents are inaccessible to it. Same-origin iframe contents can be rendered recursively.
What should I use for exact Safari pixels?
Use a native browser screenshot facility or automate the target browser with Puppeteer or Playwright. A DOM reconstruction library cannot guarantee pixel identity.
Where should a missing filter be reported?
Reduce the page to a minimal test case, include versions and the exact declaration, and open an issue in the html2canvas project as its FAQ requests.


