83

So I have this title-screen "animation" that has the title centered on a fullscreen page and when you scroll down it becomes smaller and remains at the top of the page. Here is a working example with the expected behavior, from which I stripped all unnecessary code to make it minimal:

$(window).scroll( () => {
    "use strict";
    let windowH = $(window).height();
    let windowS = $(window).scrollTop();
    let header  = $("#header").height(); 
    
    if (windowS < windowH-header) {
        $("#title").css("transform", "scale("+(2-(windowS/($(document).outerHeight()-windowH))*2.7)+")");
        $("#header").css("transform", "translateY(0)");
        $("#inside, #content").css({
            "position": "static",
            "margin-top": 0
        });
    } else {
        $("#inside").css({
            "position": "fixed",
            "margin-top": -windowH+header
        });
        $("#content").css("margin-top", windowH);
    }
  
    $("#header").css("position", windowS > (windowH-header)/2 ? "fixed" :"static");
});
.fixed {
    position: fixed!important;
}
.wrapper {
    width: 100%;
    text-align: center;
}
.wrapper:before {
    display: table;
    content: " ";
}
.wrapper:after {
    clear: both;
}
#inside {
    width: 100%;
    height: 100vh;
    background-color: lightcoral;
    display: flex;
    align-items: center;
    justify-content: center;
}
#header {
    height: 90px;
    top: 0;
    position: sticky;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.5s;
}
#title {
    width: 100%;
    color: #fff;
    transform: scale(2);
}
#content {
    height: 1000px;
    background-color: lightblue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body>  
    <div class="wrapper">
        <div id="inside">
            <div id="header">
                <h1 id="title">Title</h1>
            </div>
        </div>
    <div id="content"></div>
</body>

Next up is the exact same snippet but with one addition: I applied a filter, which is, as far as I'm concerned, purely cosmetic: filter: brightness(1.3);.

As you can see below when you scroll half-way through the "animation" the title just disappears. When you inspect the element it still has all its properties but somehow it's gone. This is the same in Firefox and Chrome and I have no idea why. I would appreciate it a lot if someone could post a working snippet with the filter applied and explain why it didn't work before.

$(window).scroll( () => {
    "use strict";
    let windowH = $(window).height();
    let windowS = $(window).scrollTop();
    let header  = $("#header").height(); 
    
    if (windowS < windowH-header) {
        $("#title").css("transform", "scale("+(2-(windowS/($(document).outerHeight()-windowH))*2.7)+")");
        $("#header").css("transform", "translateY(0)");
        $("#inside, #content").css({
            "position": "static",
            "margin-top": 0
        });
    } else {
        $("#inside").css({
            "position": "fixed",
            "margin-top": -windowH+header
        });
        $("#content").css("margin-top", windowH);
    }
  
    $("#header").css("position", windowS > (windowH-header)/2 ? "fixed" :"static");
});
.fixed {
    position: fixed!important;
}
.wrapper {
    width: 100%;
    text-align: center;
}
.wrapper:before {
    display: table;
    content: " ";
}
.wrapper:after {
    clear: both;
}
#inside {
    width: 100%;
    height: 100vh;
    background-color: lightcoral;
    filter: brightness(1.3);        /*<<<<<<<<<<<<<<<<*/
    display: flex;
    align-items: center;
    justify-content: center;
}
#header {
    height: 90px;
    top: 0;
    position: sticky;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.5s;
}
#title {
    width: 100%;
    color: #fff;
    transform: scale(2);
}
#content {
    height: 1000px;
    background-color: lightblue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body>  
    <div class="wrapper">
        <div id="inside">
            <div id="header">
                <h1 id="title">Title</h1>
            </div>
        </div>
    <div id="content"></div>
</body>
leonheess
  • 16,068
  • 14
  • 77
  • 112

4 Answers4

129

If we refer to the specification we can read:

A value other than none for the filter property results in the creation of a containing block for absolute and fixed positioned descendants unless the element it applies to is a document root element in the current browsing context. The list of functions are applied in the order provided.

This means that your position:fixed element will be positioned relatively to the filtered container and no more the viewport. In other words, it's still fixed but inside its new containing block (the filtered container)

Here is a simplified version to illustrate the issue:

.container {
  display: inline-block;
  width: 200px;
  height: 200vh;
  border: 1px solid;
}

.container>div {
  position: fixed;
  width: 100px;
  height: 100px;
  background: red;
  color: #fff;
}
<div class="container">
  <div>I am fixed on scroll</div>
</div>

<div class="container" style="filter:grayscale(1);">
  <div>I move with the scroll</div>
</div>

To fix the issue try to move the filter to the fixed element instead of its container:

.container {
  display: inline-block;
  width: 200px;
  height: 200vh;
  border: 1px solid;
}

.container>div {
  position: fixed;
  width: 100px;
  height: 100px;
  background: red;
  color: #fff;
  filter: grayscale(1);
}
<div class="container">
  <div>I am fixed on scroll</div>
</div>

Here is a non-exhaustive1 list of the properties that results in the creation of a containing block for absolute and fixed positioned descendants

  • filter
  • transform ref
  • backdrop-filter ref
  • perspective ref
  • contain ref
  • container ref
  • transform-style ref
  • content-visibility ref
  • will-change when used with one of the above values

If any non-initial value of a property would cause the element to generate a containing block for absolutely positioned elements, specifying that property in will-change must cause the element to generate a containing block for absolutely positioned elements. ref


1: I will try to keep this list up to date.

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • 11
    Thank you! I still wonder why this is a thing. Wouldn't it be better if this wasn't the case? – leonheess Oct 23 '18 at 08:12
  • 1
    @MiXT4PE it's defined in the spec so it should be like that, you may probably ask why they define it this way .. I am pretty sure there is a reason (probably a complex one) behind this choice. – Temani Afif Oct 23 '18 at 08:43
  • What if I cannot move my HTML because it needs to be inside a form which is well inside the filtered element? Is there another solution to this bug in the specs? – ygoe Nov 25 '18 at 18:58
  • 1
    @ygoe first it's not bug, it's by design like many features that we don't like but we should work with (clear float, margin collapsing, etc) so you should adapt your code in order to overcome this. There is no universal solution but depending on your case there is for sure a solution. You may ask a question by adding your particular code in order to see if we can modify your logic to keep your requirement and fix the issue. – Temani Afif Nov 25 '18 at 19:05
  • 6
    Further context for your answer: [this GitHub issue](https://github.com/w3c/fxtf-drafts/issues/11) added the specification that a containing block is created when `filter` has a value other than `none`. The GitHub issue also has a link to the discussion where this was decided, and [this code example](http://fiddle.jshell.net/mkud1Lnm/1/) ([archive link](https://web.archive.org/web/20160405103427/http://fiddle.jshell.net/mkud1Lnm/1/)) was used to illustrate the ambiguity the containing block was designed to fix. – Graham Jan 13 '19 at 20:21
  • @LonnieBest if you think it's ridiculous, you can raise a github issue and discuss with the community and the users who define the Spec. It's not a group of isolated people taking random desicion without any logical reason. There is always a discussion and a reflection behind. You should probably take note of it before drawing such conclusion – Temani Afif Jul 03 '20 at 14:48
  • 1
    Spec Issue [402](https://github.com/w3c/fxtf-drafts/issues/402). Firefox bug [1650522](https://bugzilla.mozilla.org/show_bug.cgi?id=1650522). – Lonnie Best Jul 04 '20 at 07:53
  • How to I apply it to the root? I tried doing it to :root but it still moved everything –  Jul 02 '21 at 19:55
  • @Onir you should not apply it to root. Applying it to root or body make things even worse (related: https://stackoverflow.com/a/61265706/8620333) – Temani Afif Jul 02 '21 at 19:57
  • 1
    I'm trying to get a stylus filter working and I want to invert the whole page but I don't know how to get it working without it messing the positions. –  Jul 02 '21 at 19:58
  • @Onir I doubt inverting the whole page is the solution. You probably need to implement a dark/light mode so better consider different solution, you can find a lot that rely on changing color using CSS variables – Temani Afif Jul 02 '21 at 20:01
  • Oh I managed to get it working on chrome, they work differently it seems. –  Jul 02 '21 at 23:14
  • content-visibility should be added to this list. – Spomsoree Dec 22 '21 at 09:32
  • I'm using JS to position a tooltip next to an element the mouse pointer is over. It has worked very well until I needed to use `filter` elsewhere in the document. getBoundingClientRect() gives me the position relative to the viewport, but `left` and `top` are now relative to the containing block. I suppose I could iterate up the DOM tree, find the offset of the element with filter and compensate for it. Is there a simpler way though? – Magnus Gustavsson May 30 '23 at 11:44
  • Using `offsetParent` to find the offset and compensate for it worked well enough. – Magnus Gustavsson May 30 '23 at 13:25
4

You can make it happen by using before. instead of assigning the filter: brightness(1.3); to the child element directly. assign color and filter to the before of this child element. so your code should be like this:

  #inside:before{
    filter: brightness(1.3);
    content: "";
    background-color: lightcoral;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    position: absolute;
}

This will fix the problem.

You should also edit some of your code to make this fully work:

#inside{
    position: relative;
}

Make the parent position to be relative to make sure the before wrap inside its parent.

This solution also works with other filters like backdrop-filter.

sajjad rezaei
  • 945
  • 1
  • 10
  • 23
2

In order to avoid hard maintenance and different implementation for each browser, you can use a recursive tree traversal function which marks the position 'fixed' nodes and then apply the needed filter properly without destroying the positions.

I used this code in order to apply the 'filter invert' property globally, without hard-coding it to specific parent elements.

1

If you want to blur or grayscale the entire page except one element, just use backdrop-filter instead of filter. At the time of writing Firefox just needs to ship it by default.

Not working:

<!DOCTYPE html><html><head>
    <style>
    .overlay {
        position:fixed;
        right:10%;
        top:10%;
        bottom:10%;
        left:10%;
        background-color:red;
        z-index:2;
    }
    .overlay ~ * {
        filter: grayscale(50%) blur(5px);
    }
    </style>
</head>
<body>
    <div class="overlay">I am not blurred</div>

    <!-- position test -->
    <div style="margin-top:100px;float:right;">
        <div style="background-color:red;position:fixed;top:0;right:0;width:100px;height:100px;"></div>
    </div>
</body>
</html>

Working:

<!DOCTYPE html><html><head>
    <style>
    .overlay {
        position:fixed;
        right:10%;
        top:10%;
        bottom:10%;
        left:10%;
        background-color:red;
        z-index:2;
    }
    body:after {
        content:"";
        position:fixed;
        z-index: 1;
        top:0;
        left:0;
        width:100%;
        height:100%;
        backdrop-filter: grayscale(50%) blur(5px);
    }
    </style>
</head>
<body>
    <div class="overlay">I am not blurred</div>

    <!-- position test -->
    <div style="margin-top:100px;float:right;">
        <div style="background-color:red;position:fixed;top:0;right:0;width:100px;height:100px;"></div>
    </div>
</body>
</html>
David Vielhuber
  • 3,253
  • 3
  • 29
  • 34