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
andbody
may give their own background to the “document’s canvas”. The basic workflow is- if
html
‘s background is notnone
and nottransparent
, use that for “document’s canvas”. - else if
body
‘s background is notnone
and nottransparent
, use that for “document’s canvas”. - else do whatever you want (usually browsers render white solid color).
- if
-
“Post-Processing” effects like
filter
andopacity
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.
- 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.
- 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.
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.
- 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.
- 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.
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.