That Time a Grayscale Filter Broke All My Popups
Why adding a filter style to body element breaks fixed items?

Set position: fixed on an element—where does it stick? The viewport, obviously. Except when your accessibility plugin adds a grayscale filter to the body.
Turns out, fixed elements don’t always anchor to the viewport. They anchor to their containing block. Usually that’s the viewport. But certain CSS properties—like filters—create a new containing block.
So when .pojo-a11y-grayscale hits your body, every fixed element now positions relative to <body> instead of the viewport. Your sticky nav, modals, and bottom-right buttons tumble into the pits of your page, right below the footer. They’re still “fixed,” just fixed to the wrong thing.
- filter on
- nothing
- containing block
- viewport
- chat bubble
- on screen
Tick Grayscale. Keep an eye on the bottom-right corner.
body.pojo-a11y-grayscale { filter: grayscale(100%); -webkit-filter: grayscale(100%); -webkit-filter: grayscale(1); -moz-filter: grayscale(100%); -ms-filter: grayscale(100%); -o-filter: grayscale(100%); filter: gray;}In fact any of the below mentioned rules will cause the same effect
body { transform: translateZ(0); will-change: transform; perspective: 1000px; filter: grayscale(100%);}
But why?
Rendering pipeline necessity
When the browser applies a filter or transform, it needs to create an isolated rendering layer. Everything inside that layer—including fixed elements—must be part of the same visual effect.
position: fixed hangs fromTied to the glass. Scroll all you like.
perspective, backdrop-filter, contain: paint.The Problem They’re Solving Imagine this scenario:
<div style="filter: blur(10px);"> <p>This text should be blurry</p> <div style="position: fixed;">Should this be blurry?</div></div>If the fixed element stayed viewport-relative, it would:
- Render outside the filtered layer
- Not receive the blur effect
- Break the visual hierarchy (child escapes parent’s visual effect)
This text should be blurry
A fixed child, sealed inside a blurred div.
You may not think of these reasons as a problem but for browsers and w3c they are, because they go against the css philosophy of cascade. Technically an exception could be made that would require a more expensive render.
How to fix it?
You can fix the issue in two ways:
- remove the style from parent element that makes it the
containing block - specify position for fixed items relative to the new containing block
Back to the WordPress site with the Pojo Accessibility plugin. On click, it adds .pojo-a11y-grayscale to the body: the block from the top of this post. Everything after its first line is vendor prefixes and legacy syntax for browsers that are mostly gone, so I’ll skip that wall below and write plain filter. Copy the prefixes back in if you still care about ancient browsers.
I would override it like this:
body.pojo-a11y-grayscale { filter: unset;}
html:has(body.pojo-a11y-grayscale) { filter: grayscale(100%);}The benefit: we don’t touch the JS, we don’t change how the plugin or its triggers work, and it’s explicit. The effect just moves from body to html. And html is the one exception in the spec: a filter on the root element doesn’t create a new containing block, so all the fixed items go back to the viewport.
body.pojo-a11y-grayscale {
filter: grayscale(100%);
}The plugin’s way. Body grabs the popup, off-screen.
filter gets the root pass; transform on html still grabs.Another option
is to specify siblings instead of ancestor. If you have a structure like this
<html> <body> <div style="position:fixed">...</div> <article>...</article> </body></html>you can run the filter on the level of the fixed block like this:
/* Remove the plugin's filter from body */body.pojo-a11y-grayscale { filter: unset;}
body.pojo-a11y-grayscale > * { filter: grayscale(100%);}Either way, the popups climb back out of the footer. Next time something position: fixed stops being fixed, don’t debug the element. Walk up its ancestors and look for filter, transform, perspective or will-change. Odds are one of them is squatting on your viewport.
See also: Fixing a Font, another CSS bug on another old WordPress site.
Comments
Comments live in GitHub Discussions (via giscus), so you need a GitHub account to join in.