Почему применение CSS-фильтра к родительскому элементу нарушает позиционирование дочернего элемента?

Итак, у меня есть «анимация» титульного экрана, в которой заголовок Title сосредоточен на полноэкранной странице, и при прокрутке вниз он становится меньше и остается в верхней части страницы. Вот рабочий пример с ожидаемым поведением, из которого я удалил весь ненужный код, чтобы сделать его минимальным:

$(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>

Далее идет тот же самый фрагмент, но с одним дополнением: я применил фильтр, который, на мой взгляд, носит чисто косметический характер: filter: Brightness(1.3);.

Как вы можете видеть ниже, когда вы прокручиваете половину «анимации», заголовок просто исчезает. Когда вы проверяете элемент, он по-прежнему сохраняет все свои свойства, но каким-то образом его нет. То же самое происходит в Firefox и Chrome, и я понятия не имею, почему.
Я был бы очень признателен, если бы кто-нибудь опубликовал рабочий фрагмент с примененным фильтром и объяснил, почему он не работал раньше.

$(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>

Свободный перевод вопроса Почему применение CSS-фильтра к родительскому элементу нарушает позиционирование дочернего элемента? от участника @leonheess.


Ответы (3 шт):

Автор решения: puffleeck

$(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: 250px;   /* !!!!!!!!!!!!!!
    по всплывашке на потолке в консоли, можно было
    догадаться, что оно уехало куда то вверх. FF 91.6
    а вот почему уехало... это уже хороший вопрос)))
    fixed fixed'у родитель, контейнер и запасной потолок :3
    похоже top: 0 то на край окна, то на край родителя
    тригерится, начхав на то, что тот fixed в обоих случаях.
    учитывая что он должен цепляться за окно только
    если родитель static... выходит вопрос не в том почему
    исчезал с фильтром, вопрос в том, почему не исчезал
    без него :3 */
    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>

→ Ссылка
Автор решения: HTO HOT

На вопрос почему это так работает, я к сожалению ответить не смогу (возможно это баг, или фича для фикса неких багов). Я попытался разобраться и видимо блок с filter выступает как некий relative блок в том числе и для дочерних блоков с position:fixed. И это при том, что сам блок с filter имеет position:static.

const wrap = document.querySelector('.wrap');
const someBlock = document.querySelector('.some-block');
const toggleF = document.querySelector('button');
const radioElems = document.querySelectorAll('input[name="position"]');
radioElems.forEach(elem => {
    elem.addEventListener('change', (e) => {
        if (e.target.checked) {
            someBlock.style.position = e.target.value;
            if (e.target.value === 'static') {
                someBlock.style.transform = 'translate(0,0)'
            } else {
                someBlock.style.transform = 'translate(-50%,-50%)'
            }
        }

    })
})
toggleF.addEventListener('click', () => {
    wrap.classList.toggle('filter')
})
html,
body {
    margin: 0;
    padding: 0;
}

.filter {
    filter: brightness(1.3);
}

.wrap {
    height: 100px;
    background-color: brown;
    width: 100%;
    position: static;
}

.some-block {
    color: blue;
    font-weight: 700;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 20px;
    border: 2px black solid;
}

.long-block {

    height: 200vh;
    background-color: aqua;
}
<div class="wrap">
        <div class="some-block">Block</div>
    </div>
    <div class="long-block">
        <button>Toggle Filter</button>
        <div>
            <p>Свойство position для "some-block"</p>
            <div>
                <label>absolute <input checked type="radio" value="absolute" name="position" id="absolute"></label>
            </div>
            <div>
                <label>static <input type="radio" value="static" name="position" id="static"></label>
            </div>
            <div>
                <label>fixed <input type="radio" value="fixed" name="position" id="fixed"></label>
            </div>


        </div>

    </div>

Моя попытка исправить ЭТОТ КОД, но НЕ ТАКОЕ ПОВЕДЕНИЕ filter

$(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);
    }
    //// Здесь просто удалил position fixed для header
});
.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>

P.S. Немного погуглив наткнулся на ещё несколько свойств которые работают также. К примеру transform:translate(0,0) точно также изменит поведение дочернего absolute или fixed элемента.

→ Ссылка
Автор решения: Alexandr_TT

Если обратиться к спецификации, то можно прочитать:

Значение свойства фильтра, отличное от none, приводит к созданию содержащего блока для потомков с абсолютным и фиксированным расположением, если только элемент, к которому оно применяется, не является корневым элементом документа в текущем контексте просмотра. Список функций применяется в указанном порядке.

Это означает, что ваш элемент position:fixed будет позиционироваться относительно фильтруемого контейнера, а не относительно области просмотра. Другими словами, он все еще fixed, но внутри нового содержащего блока (фильтрованного контейнера).
Вот упрощенная версия, иллюстрирующая проблему:

.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>

Чтобы решить эту проблему, попробуйте переместить фильтр в фиксированный элемент, а не в его контейнер:

.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>

Вот неполный1 список свойств, который приводит к созданию содержащего блока для абсолютных и фиксированных потомков:

  • filter
  • transform ref
  • backdrop-filter ref
  • perspective ref
  • contain ref
  • container ref
  • transform-style ref
  • content-visibility ref
  • will-change при использовании с одним из вышеуказанных значений

Если какое-либо не начальное значение свойства приведет к тому, что элемент сгенерирует содержащий блок для абсолютно позиционированных элементов, указание этого свойства в will-change должно привести к тому, что элемент сгенерирует содержащий блок для абсолютно позиционированных элементов. ref


1: Я постараюсь поддерживать этот список в актуальном состоянии..

PS

Перевод наиболее значимых комментариев:

@MiXT4PE это определено в спецификации, так что так и должно быть. Вы, вероятно, можете спросить, почему они определяют это именно так ... Я почти уверен, что за этим выбором стоит причина (вероятно, сложная). – @Temani Afif

@ygoe во-первых, это не bug, это задумано, как и многие функции, которые нам не нравятся, но с которыми мы должны работать (очистить плавающее пространство, схлопывать поля и т. д.), поэтому вам следует адаптировать свой код, чтобы преодолеть это. Универсального решения не существует, но в зависимости от вашего случая наверняка найдется решение. Вы можете задать вопрос, добавив свой конкретный код, чтобы посмотреть, сможем ли мы изменить вашу логику, чтобы удовлетворить ваши требования и решить проблему. – @Temani Afif

Свободный перевод ответа от участника @Temani Afif.

→ Ссылка