Почему применение 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 шт):
$(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>
На вопрос почему это так работает, я к сожалению ответить не смогу (возможно это баг, или фича для фикса неких багов). Я попытался разобраться и видимо блок с 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 элемента.
Если обратиться к спецификации, то можно прочитать:
Значение свойства фильтра, отличное от
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 список свойств, который приводит к созданию содержащего блока для абсолютных и фиксированных потомков:
filtertransformrefbackdrop-filterrefperspectiverefcontainrefcontainerreftransform-stylerefcontent-visibilityrefwill-changeпри использовании с одним из вышеуказанных значений
Если какое-либо не начальное значение свойства приведет к тому, что элемент сгенерирует содержащий блок для абсолютно позиционированных элементов, указание этого свойства в
will-changeдолжно привести к тому, что элемент сгенерирует содержащий блок для абсолютно позиционированных элементов. ref
1: Я постараюсь поддерживать этот список в актуальном состоянии..
PS
Перевод наиболее значимых комментариев:
@MiXT4PE это определено в спецификации, так что так и должно быть. Вы, вероятно, можете спросить, почему они определяют это именно так ... Я почти уверен, что за этим выбором стоит причина (вероятно, сложная). – @Temani Afif
@ygoe во-первых, это не bug, это задумано, как и многие функции, которые нам не нравятся, но с которыми мы должны работать (очистить плавающее пространство, схлопывать поля и т. д.), поэтому вам следует адаптировать свой код, чтобы преодолеть это. Универсального решения не существует, но в зависимости от вашего случая наверняка найдется решение. Вы можете задать вопрос, добавив свой конкретный код, чтобы посмотреть, сможем ли мы изменить вашу логику, чтобы удовлетворить ваши требования и решить проблему. – @Temani Afif
Свободный перевод ответа от участника @Temani Afif.