Меня зовут София. Я - CSS инженер. Это мой блог.

Если вам очень больно без стилей, вы можете их.

.

Интересный случай с селекторами

Недавно CSSWG рассматривало ишью о разнице в поведении селекторов :has(:is()) и просто :has(). Обсуждение показалось мне очень интересным и я хочу подсветить и разобрать его.

Про has и is

Функциональный псевдокласс is() сопоставляется с любым селектором из переданного ему в качестве аргумента списка. То есть, например, здесь тексты в обоих блоках будут красного цвета:

<div class="a">A</div>
<div class="b">B</div>

<style>
  div:is(.a, .b) { color: red; }
</style>

Функциональный псевдокласс has() сопоставляется с теми боксами, которые имеют ниже по дереву указанных в аргументе потомков. То есть, например, в этом случае только первый текст будет красным:

<div class="a">A <span></span></div>
<div class="b">B</div>

<style>
  div:has(span) { color: red; }
</style>

Про is внутри has

Очевидно, что мы легко может помещать is внутрь has. Например, в этому случае красными будут два нижних текста:

<div class="a">A</div>
<div class="b">B <i></i></div>
<div class="c">C <span></span></div>

<style>
  div:has(:is(span, i)) { color: red; }
</style>

Мы даже может без проблем сконструировать такой селектор:

<div class="c">C <span><i></i></span></div>

<style>
  div:has(:is(span i)) { color: red; }
</style>

Однако, что будет, если мы напишем первым селектором внутри is что-то, что выходит за область видимости селектора перед has?

Про утечку из области видимости

В этом разделе я буду использовать текст в псевдоэлементах как маркер, потому что color - свойство наследуемое, а в примерах блоки вложены друг в друга.

Рассмотрим такую конструкцию:

<div id="outer">outer
  <div id="middle">middle
    <div id="inner">inner</div>
  </div>
</div>

<style>
  div:has(#outer #inner)::before {
    content: "1_"
  }
</style>

Ни к одному из имеющихся элементов этот селектор не применится. Потому что has пытается найти внутри свой области видимости весь селектор-аргумент сразу. Оно ищет внутри себя #outer в который был бы вложен #inner. И не находит, потому что самый внешний блок, к которому применяется часть селектора до has (селектор по тегу div) - это блок #outer. А внутри него нет другого блока #outer. Это все равно, что написать #outer:has(#outer #inner). Очевидно, что #outer не находится сам внутри себя.

Попробуем изменить ситуацию:

<div id="outer">outer
  <div id="middle">middle
    <div id="inner">inner</div>
  </div>
</div>

<style>
  div:has(:is(#outer #inner))::before {
    content: "1_"
  }
</style>

Теперь has пытается найти внутри себя #inner, который являлся бы потомком #outer. И это работает. Под условие подходят блоки #middle и #outer. Оба имеют внутри себя #inner, одним из предков которого является #outer.

Получается, что has в этом случае как бы смотрит дальше свой области видимости.

Приведу так же более утрированный пример ради лучше иллюстрации:

<div id="outer">outer
  <div>div
    <div>div
      <div>div
        <div id="middle">middle
          <div id="inner">inner</div>
        </div>
      </div>
    </div>
  </div>
</div>

<style>
  #middle:has(:is(#outer #inner))::before {
    content: "1_"
  }
</style>

До has стоит прямое указание на блок #middle, поэтому has начинает искать вглубь него. Он ищет там блок #inner, один из предков которого был бы блок #outher. И находит его.

И всё нормально.

В результате обсуждения такое поведение было признано правильным. Пользуйтесь и будьте внимательны.