ScreenshotNeo

BlogHow-to

How to Handle CSS Transforms That html2canvas Does Not Support

Diagnose html2canvas transform failures, build reliable workarounds, and choose a browser-rendered screenshot when DOM reconstruction is not enough.

By the ScreenshotNeo team1 October 20269 min read

Short answer: html2canvas does not take a native screenshot. It walks the DOM and rebuilds a canvas from the CSS properties it knows. The official feature list marks transform as Limited support, so rotations, scales, 3D transforms, transform functions, or transformed geometry can differ from what the browser displays.

Start by recording the exact html2canvas version, browser, element, computed transform value, and expected versus actual pixels. Reduce the page to a minimal reproduction. If the transformed geometry is not essential, remove or simplify the transform only in the capture copy and restore the live page. If pixel fidelity is required, use a capture method that renders the browser rather than reconstructing the DOM.

What html2canvas actually supports

The official feature list includes transform with the qualification “Limited support.” The project explains that every CSS property must be implemented manually, so it cannot provide complete CSS support; see the FAQ.

The documentation describes a DOM-based reconstruction: html2canvas traverses the document, reads styles and content, and paints its own representation. It does not capture the browser’s already-composited pixels. A transform can therefore be parsed partially, applied with different geometry, or ignored while other parts of the element still render.

Repository source at master shows handlers for matrix() and matrix3d(). An unknown transform function causes an unsupported-function error. The matrix3d() handler extracts a 2D representation and includes a comment that general 3D transforms are not supported. Treat this as evidence about that source revision, not a promise about every released package: verify your installed version.

Diagnose a transform failure systematically

  1. Record the environment. Save the package version from your lockfile, browser and browser version, operating system, target selector, and capture options. A repository master implementation can differ from the npm release you installed.
  2. Inspect computed style. Check the complete value returned by getComputedStyle(element).transform. It may be none, a six-number matrix(...), a sixteen-number matrix3d(...), or a function such as rotate(), scale(), or perspective().
  3. Compare geometry. Log getBoundingClientRect(), scroll offsets, and the element’s width and height. A correct transform matrix can still produce a surprising crop if the capture rectangle or scroll position is wrong.
  4. Remove unrelated variables. Keep the failing element, its transform, required fonts and images, and the smallest parent structure. The FAQ recommends a test case when a CSS property is missing or incomplete.
  5. Try a capture-only style. If the transform is decorative, temporarily set it to none or replace it with a simpler 2D layout in the cloned document. Compare the result with the product requirement before adopting the workaround.
  6. Separate transform problems from resource problems. Cross-origin images, tainted canvases, inaccessible iframes, missing fonts, and canvas size limits can create blank or incomplete output independently of transform parsing.

Diagnostic snippet

import html2canvas from 'html2canvas';

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

const style = getComputedStyle(target);
const rect = target.getBoundingClientRect();

console.table({
  html2canvasVersion: 'record from package.json or lockfile',
  browser: navigator.userAgent,
  transform: style.transform,
  transformOrigin: style.transformOrigin,
  width: rect.width,
  height: rect.height,
  left: rect.left,
  top: rect.top,
  devicePixelRatio: window.devicePixelRatio
});

html2canvas(target, { logging: true }).then(canvas => {
  document.body.appendChild(canvas);
});

Build a minimal reproduction

Use a plain element and one transform first. Then add the real layout one variable at a time.

<div id="capture" class="card">Transform test</div>
<style>
  #capture {
    width: 240px;
    height: 120px;
    background: #4f46e5;
    color: white;
    transform: rotate(12deg) scale(1.1);
    transform-origin: center;
  }
</style>
<script type="module">
  import html2canvas from 'html2canvas';
  const canvas = await html2canvas(document.querySelector('#capture'));
  document.body.append(canvas);
</script>

Change one variable per run: remove rotate, then scale, then the transform origin, then nested transforms. If the simple case works but the production case fails, inspect containing blocks, overflow clipping, fixed positioning, pseudo-elements, fonts, images, and nested transforms.

Workaround 1: remove the transform only while capturing

This is appropriate when the transform is decorative and the untransformed layout is an acceptable image. Always restore the original inline style in a finally block, including when html2canvas rejects.

import html2canvas from 'html2canvas';

async function captureWithoutTransform(selector) {
  const element = document.querySelector(selector);
  if (!element) throw new Error(`No element matches ${selector}`);

  const previous = element.style.transform;
  const previousOrigin = element.style.transformOrigin;

  try {
    element.style.transform = 'none';
    element.style.transformOrigin = 'initial';
    await new Promise(requestAnimationFrame);

    return await html2canvas(element, {
      backgroundColor: null,
      logging: true
    });
  } finally {
    element.style.transform = previous;
    element.style.transformOrigin = previousOrigin;
  }
}

const canvas = await captureWithoutTransform('#capture');
document.body.appendChild(canvas);

Because changing a transform can change layout, stacking, clipping, and the element’s bounds, check the resulting dimensions and downstream alignment. Do not use this workaround when the transformed geometry is the thing you need to document.

Workaround 2: modify the cloned document with onclone

The configuration reference provides onclone, called after html2canvas clones the document. You can alter only the capture copy, leaving the live page untouched.

import html2canvas from 'html2canvas';

const target = document.querySelector('#capture');
const canvas = await html2canvas(target, {
  onclone: clonedDocument => {
    const cloned = clonedDocument.querySelector('#capture');
    if (!cloned) return;

    cloned.style.transform = 'none';
    cloned.style.transformOrigin = 'initial';
    cloned.style.boxShadow = 'none';
  },
  backgroundColor: '#ffffff',
  scale: window.devicePixelRatio,
  logging: true
});

document.body.appendChild(canvas);

This is safer for interactive applications because users do not see a flash of untransformed content. It still changes the capture’s geometry, so document that choice in your visual regression expectations.

Workaround 3: replace a complex transform with capture-only layout

For a 3D card, perspective effect, or nested transform chain, create a capture class that expresses the intended composition with ordinary dimensions and positioning. Keep the production transform for the live UI and apply the class in onclone.

/* Production appearance remains transformed. */
.card {
  transform: perspective(700px) rotateY(18deg) rotateX(6deg);
}

/* Approximate capture layout. */
.capture-flat .card {
  transform: none;
  width: 320px;
  height: 200px;
  margin: 24px;
}

const canvas = await html2canvas(document.querySelector('#scene'), {
  onclone: clonedDocument => {
    clonedDocument.documentElement.classList.add('capture-flat');
  }
});

This produces a predictable illustration of the content, not a pixel-equivalent rendering of the transformed browser surface. Use it only when that distinction is acceptable.

Understand relevant configuration options

Option Why it matters
onclone Apply capture-only CSS without changing the source document.
foreignObjectRendering Uses foreignObject rendering when the browser supports it. Test it in your target browsers; it is not a universal transform fix.
scale Controls output resolution; the default is the device pixel ratio. Higher values increase canvas memory and size.
x, y, width, height Control the capture rectangle. A correct transform can appear clipped when these values exclude the transformed bounds.
scrollX, scrollY Set the scroll position used for rendering, especially for fixed-position elements.
windowWidth, windowHeight Set the virtual viewport and media-query environment. The FAQ suggests sizing these from the element for large captures.
cullOffscreen Controls conservative painting of transformed nodes when culling is enabled. It does not add support for transform syntax.
ignoreElements / data-html2canvas-ignore Remove overlays, chat widgets, or controls that obscure diagnosis.
backgroundColor Use null for a transparent canvas or a fixed color for deterministic output.
useCORS, allowTaint, proxy Address cross-origin image rules; these options cannot make an inaccessible iframe readable.

When to use a browser-rendered capture instead

If the requirement is “match exactly what a user sees,” DOM reconstruction is the wrong fidelity model. Use a browser automation or screenshot service that captures the rendered browser surface. Choose based on whether you can run a browser, how you handle authentication and cross-origin resources, latency, operational maintenance, and cost.

Troubleshooting common failures

Symptom Likely cause Fix
Attempting to parse an unsupported transform function The parser does not recognize a transform function in your installed release. Record the exact version, reduce to a test case, replace the function for capture, or use browser-rendered capture.
Rotation works but 3D depth is wrong matrix3d() handling may extract a 2D representation; general 3D support is not implied. Flatten the capture layout or switch capture methods.
Only part of a rotated element appears Capture bounds, overflow, scroll offsets, or offscreen culling exclude transformed pixels. Inspect getBoundingClientRect(), remove clipping in the clone, set appropriate x/y/width/height, and test culling behavior.
The result is blank or cuts off Canvas size limits, cross-origin images, tainted canvases, or inaccessible iframes. Reduce dimensions or scale, set window dimensions deliberately, use CORS or a proxy where permitted, and isolate external resources.
Layout changes between runs Fonts, images, animations, lazy content, or media queries are not settled. Wait for fonts and images, disable animations in onclone, fix viewport dimensions, and capture after the target is stable.
The live page flashes flat The source element was modified directly before capture. Move the style change into onclone or restore it in finally.
Capture differs after upgrading Parser and browser behavior can vary by release. Pin the version, rerun the minimal reproduction, and update visual baselines deliberately.

Performance, reliability, and cost considerations

  • Keep the reproduction small. Large DOM trees increase traversal and painting work and make failures harder to attribute.
  • Control pixel count. A high scale multiplies width and height in memory. Large canvases can hit browser-specific limits.
  • Wait once, capture once. Ensure fonts, images, lazy content, and animations are settled before invoking html2canvas. Repeated retries do not add transform support.
  • Make output deterministic. Fix viewport and background settings, disable transitions in the clone, and pin html2canvas versions for visual tests.
  • Plan for external resources. Same-origin policy and iframe restrictions can dominate reliability even after the transform is simplified.
  • Budget engineering time. A temporary CSS workaround has low infrastructure cost but can diverge from the live geometry. Browser automation or an API has operational or per-capture cost but is a better fit for rendered-pixel fidelity.

Or skip the browser setup

ScreenshotNeo captures a URL with a single request, so you do not need to reproduce the page inside html2canvas or maintain browser setup. Before the capture it accepts the cookie or consent banner like a visitor 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 identify the page verdict and whether it was billed.

For a rendered page screenshot:

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp
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)
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 image = Buffer.from(await res.arrayBuffer());

See the ScreenshotNeo API documentation for request options. ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. 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.

Reporting an unsupported transform upstream

Open an issue only after you can provide a small, repeatable case. Include:

  • the exact html2canvas package version and browser version;
  • the smallest HTML and CSS that reproduces the mismatch;
  • the computed transform and transform-origin values;
  • the html2canvas options;
  • the expected browser rendering and actual canvas output; and
  • whether cross-origin images, iframes, custom fonts, scrolling, or clipping are involved.

This gives maintainers a test case for a missing or incomplete property, as requested in the official FAQ.

FAQ

Does html2canvas support transform: rotate()?

Transform support is limited. A simple rotation may work in one release and context while nested transforms, clipping, or other functions still differ. Verify with your target version and a minimal case.

Can I make html2canvas support every 3D transform with an option?

No configuration option adds general 3D transform support. The reviewed parser source extracts a 2D representation from matrix3d(); use a capture-only flat layout or a browser-rendered method when depth matters.

Is foreignObjectRendering a guaranteed fix?

No. It can use a different browser rendering path when supported, but browser support and resource restrictions still apply. Test it in the browsers you ship.

Why does removing the transform change the element’s size?

Transforms affect the displayed geometry and can interact with overflow, positioning, and stacking. Removing one changes the capture layout, so compare bounds and decide whether the simplified image is acceptable.

Should I set allowTaint: true to fix a transform issue?

No. That option concerns cross-origin images and canvas tainting. It does not implement missing transform syntax and can make the canvas unreadable.

What is the most reliable way to match the browser pixels?

Capture the rendered browser surface with browser automation or a screenshot API. html2canvas remains useful when a DOM-based approximation is sufficient and you need a client-side canvas.