CSS filter:invert not working with background-color

TL;DR: Don’t mess too much with html element.

The easy workaround is to block body‘s background propagation to the document’s canvas, but make it take the same size as the html by removing its margin, and applying all the styles you had on html on the body, and the ones you had on the body to a wrapper <div>.

html {
  /* block body's background propagation */
  background: #FFF;
}


/* move all one layer down */
body {
  filter: invert(1);
  background: #fff;
  padding: 50px;
  /* make it cover the full canvas */
  margin: 0;
}
.wrapper {
  background-color: #0000ff;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
}

.text {
  text-align: center;
  color: red;
}
<div class="wrapper">
  <div class="text">
    If it works: color should not be red, background should not be blue and border should not be white
  </div>
</div>

More in depth observations:

There are a few concepts at play here, and their interaction is not that easy to grasp (at least to me…).

  • “Rendering Layers”: When painting a page there will generally be several layers of rendering, possibly nested on which effects like filters or opacity will be applied. The specs only define “stacking contexts”, for our case here after they are the same thing.
  • “Document’s canvas”: Each document has a background canvas, which is not present in the DOM and which stands as the deepest “rendering layer”.
  • “Background propagation”: Some special elements have special behaviors regarding their CSS background property. Notably, html and body may give their own background to the “document’s canvas”. The basic workflow is

    • if html‘s background is not none and not transparent, use that for “document’s canvas”.
    • else if body‘s background is not none and not transparent, use that for “document’s canvas”.
    • else do whatever you want (usually browsers render white solid color).
  • “Post-Processing” effects like filter and opacity should apply on a whole “rendering layer” when all its inner content already has been rendered.

  • Setting such “Post-Processing` effects on an element should isolate that element and create a new “rendering layer” from it.

Now, it’s very unclear how the “document’s canvas” should be affected by these “post-processing” effects, and I couldn’t find any definitive answer to this case in the specs.

What’s for sure, is that we have [Compat] issues in there…

Not only not all browsers do follow the same rules, but some browsers will behave differently when the page is presented as a standalone window, or in an iframe.

Since the test results do vary between windowed and framed renderings, and that StackSnippet only allows framed rendering, I am forced to outsource the test case in this plnkr.

html {
  background: red;
  height: 50vh;
  border: 10px solid green;
}

.opacity {
  opacity: 0.5;
}

.filter {
  filter: invert(1);
}

body {
  background: yellow;
  margin: 10vh;
  border: 2px solid green;
}

The results from these tests for majors browsers are:

When windowed: (screenshot orders, from left to right: nothing, filter, opacity, filter + opacity).

  • Firefox doesn’t apply neither filter nor opacity to the document’s canvas. Firefox screenshots
  • Edge doesn’t apply neither filter nor opacity to the document’s canvas. (same as Firefox)
  • Chrome < 81 doesn’t apply neither filter nor opacity to the document’s canvas. (same as Firefox)
  • Chrome >= 81 applies both filter and opacity to the document’s canvas.
    Chrome screenshots
  • Safari does
    • apply the filter uniformly when there is no opacity set.
    • not apply the opacity on the document’s canvas
    • create a new layer for <html> when opacity is set and applies both opacity and filter on the <html>‘s background.
      However it now uses <body>‘s background color as the document’s canvas… but let it unaffected by the filter.

    Safari screenshots

When framed: (screenshot orders, from left to right: nothing, filter, opacity, filter + opacity).

  • Firefox doesn’t apply neither filter nor opacity to the document’s canvas.
    Firefox screenshots
  • Edge doesn’t apply neither filter nor opacity to the document’s canvas. (same as Firefox)
  • Chrome (all versions) doesn’t apply neither filter nor opacity to the document’s canvas.
    Chrome screenshots
  • Safari does
    • apply the filter uniformly when there is no opacity set.
    • set the document’s canvas to transparent when opacity is set, and create a new layer for <html> on which the opacity is applied.
    • create a new layer for <html> when opacity is set and applies both opacity and filter on the <html>‘s background.
      However it now sets the document’s canvas transparent.

    Safari screenshots


So once again, I don’t know if any result here is per specs, what I know is that as web-authors, we should avoid messing with it when possible.


Post-scriptum:

  • Here is the Chromium issue from which the new Chrome behavior was introduced.
  • Here is a proposal to allow web authors to define the document’s canvas background as transparent for some devices.

Leave a Comment