Skip to content

CSS filters and layer opacity ​

This page describes the limited surface-compositing implementation (introduced in PR #239 and shipped in the 2.4.4 release; it is included in current releases). It is not a claim that every CSS filter chain matches the browser compositor. The public html2canvas(element, options) API is unchanged.

Supported combinations ​

For an eligible stacking context, the renderer captures the complete subtree, applies its filter, and then composites its CSS opacity once. This avoids applying the parent's opacity separately to overlapping children.

The surface path supports the full standard filter function set — blur(), brightness(), contrast(), grayscale(), hue-rotate(), invert(), opacity(), saturate(), sepia() and drop-shadow() — in any order and any count, via the native canvas backend:

CSS on the layerSurface pathNotes
opacity: 0.5; filter: noneSupportedOpacity-only composition; no filter capability probe or SVG encoding.
filter: blur(5px)SupportedAn additional CSS opacity property is allowed.
filter: drop-shadow(-12px 8px 6px rgba(0, 0, 0, 0.6))SupportedSigned offsets and a parsed color.
filter: blur(5px) brightness(2); opacity: 0.5Supported (native)Unitless functions are scale-invariant; declaration order is preserved.
filter: drop-shadow(0px 2px 4px black) blur(5px)Supported (native)Reversed order is expressed exactly by the native backend.
filter: blur(2px) blur(3px)Supported (native)Consecutive blurs compound; the surface outset sums their radii.
filter: drop-shadow(0px 2px 4px black) drop-shadow(8px 0px 2px red)Supported (native)Multiple CSS drop shadows preserve their order.
filter: opacity(0.5)Supported (native)Applied during filtering, before the separate layer-opacity pass.
filter: url(#my-filter)Previous rendererSVG references are passed verbatim to ctx.filter on the legacy path, which applies them natively where the runtime supports it.

SVG fallback subset. The SVG backend predates the ordered-chain parser and only expresses at most one blur() followed by at most one drop-shadow(). When native canvas filters are unavailable (older WebKit) and the chain is outside that subset, the surface path rejects the chain and the affected subtree stays on the previous renderer rather than rendering a wrong filter.

The CSS descriptor preserves filter syntax, units, functional colors and url() references. The internal parseFilterChain helper recognizes the standard set from computed CSS, whose supported lengths are in pixels. It is not a public parser for arbitrary authored CSS. Browser-resolved values are what matter; an authored relative length is not the same as passing an unresolved blur(2em) string to that helper.

Nonzero blur(5) is invalid CSS and is not repaired by appending px. Valid blur(0), blur(0px) and blur() are no-op cases after browser computation. Invalid CSS declarations are handled by the browser before capture and may leave a previous valid declaration in effect.

A supported example ​

html
<div id="example" style="position:relative;width:280px;height:220px">
  <div style="position:absolute;inset:0;opacity:0.5;
              filter:blur(4px) drop-shadow(-18px 8px 8px rgba(0,0,0,0.6))">
    <div style="position:absolute;left:80px;top:80px;
                width:80px;height:60px;background:#176b70"></div>
    <div style="position:absolute;left:120px;top:80px;
                width:80px;height:60px;background:#176b70"></div>
  </div>
</div>
js
const canvas = await html2canvas(document.getElementById('example'), {
    scale: 1,
    backgroundColor: null
});
document.body.appendChild(canvas);

The two opaque children are assembled before the layer's 50% opacity is applied. The interior overlap remains approximately alpha 128/255; it does not become darker merely because multiple child draws overlap. The regression fixture checks actual pixels rather than only successful completion.

Subtree restrictions ​

A supported filter string alone is not enough. Eligibility checks the candidate subtree and relevant ancestors up to the active surface boundary. The surface path is not used when those checks encounter a transform (including individual rotation), zoom other than 1, non-normal mix-blend-mode, a non-none clip-path, display: list-item, or an unsupported filter chain.

This restriction applies even to a layer with only CSS opacity. An unsupported ancestor or descendant can prevent that layer from using surface composition. It does not mean these CSS properties are unsupported everywhere in the library; their existing rendering paths remain available. Nested eligible filter/opacity layers, ordinary ancestor clipping and signed filter outsets have focused regression coverage.

Several box-shadow values are not the same as several filter: drop-shadow(...) functions. The former have dedicated source-painting tests; the latter remain outside this surface subset.

Two different fallback decisions ​

Native Canvas to SVG: keep surface composition ​

For an eligible filtered layer, a cached runtime pixel probe checks that Canvas can actually render blur and a colored semitransparent shadow. Browser names and the existence of a filter property alone are not used as proof of support.

When the probe succeeds, the native path filters at alpha 1 and applies layer opacity in a separate pass. When native filtering is unavailable, or a native allocation/assignment/draw fails recoverably, the renderer tries the SVG backend on the same completed surface. Both backends preserve the filter-then-opacity ordering. SVG is a backend fallback, not a return to per-draw composition. There is no new public option for forcing either backend.

Surface composition to the previous renderer: limited guarantees ​

Unsupported filter chains or subtrees remain on the previous rendering path. An eligible surface also returns to that path for the affected subtree when its raster reservation cannot be obtained, or when the remaining SVG path cannot serialize/decode/draw the source. Examples include origin-tainted pixels that cannot be serialized, a CSP that blocks the required data image, and SVG decode failure or timeout.

The previous path does not gain the surface-compositing guarantees. Parent opacity may still compound across draws, and filters unavailable in that runtime may be absent. It is not advertised as visually equivalent to either surface backend. Separate CSS parser fixes still apply, so “previous renderer” does not mean byte-identical output to every 2.4.3 capture.

The native backend needs no PNG/SVG data image and can therefore keep surface composition under a CSP that would block the SVG backend. Neither backend clears canvas origin taint. Cancellation remains an AbortError instead of being converted into a successful fallback.

Bounds, performance and known differences ​

Intermediate surfaces share a 64 MiB RGBA raster reservation, conservatively accounting for four rasters per active surface, with each side capped at 8192 raster pixels. This is not a 64 MiB total browser-memory guarantee and does not include the final output or all encoder/decoder overhead. Higher capture scales increase the raster area. Over-budget subtrees use the previous path before surface allocation, without silently downscaling the image.

The native fast path avoids the expensive PNG/SVG round trip on supported runtimes. SVG remains significantly more costly for some large, high-entropy inputs. See production native/SVG measurements for the protocol, raw-evidence links and unresolved over-budget performance gap. Passing tests does not certify a universal latency target.

In the named nested outside-shadow case, the implementation retains the complete shadow while native WebKit may clip or suppress part of it. The maintainer accepted documenting this difference; it is not pixel-equivalent to native WebKit. The design note records the separate regression policy. SVG overflow: visible paint bounds are a different, still-unfixed limitation.

Verification and further reading ​

The design note links the reproduction and independent DOM-reference regressions. The native fast-path report covers runtime probing, native/SVG comparisons, cleanup, cancellation and performance. The narrower surface subset does not remove the library's existing filter-function support.

After installing dependencies and building the branch, run:

sh
pnpm exec playwright install --with-deps chromium firefox webkit
node scripts/filter-descriptor-regressions.mjs
node scripts/filter-native-regressions.mjs
node scripts/filter-surface-benchmark.mjs

These commands complement the unit suite and the separate native macOS WKWebView probe. They do not establish correctness on every device, for general filter chains, transformed/blended surfaces or arbitrary SVG overflow.