Декодирование CSS селекторов: :has(:not) и :not(:has)
:has(:not) и :not(:has), а также то, как подходить к декодированию CSS селекторов, использующих эти вложенные CSS функции.HTML структура
Чтобы понять, что делает тот или иной CSS селектор, проще всего работать с простой HTML структурой. Воспользуемся следующими структурами:
<!-- card 1 -->
<div class="card">
<img />
<span />
</div>
<!-- card 2 -->
<div class="card">
<span />
</div>Вы наверняка писали подобный код раньше: Есть контейнер div, внутри него — произвольное изображение и span, содержащий текст.
Используя CSS, необходимо выбрать карточку, если она не содержит изображения. Для этого рассмотрим разницу между этими двумя CSS селекторами и посмотрим, как они выбирают или не выбирают один из элементов .card.
.card:has(:not(img)) {}
.card:not(:has(img)) {}Оба этих CSS селектора выбирают элемент с классом card, и оба имеют специфичность (0,1,1), но добавляют разную условную логику.
Если рассмотреть приведённые выше селекторы, есть три вещи, помогающие лучше понять, что они делают:
- Добавьте неявные селекторы, которые были пропущены.
- Разбейте селекторы на части
- Рассмотрите селектор изнутри наружу
Добавление неявных селекторов
Когда есть CSS функция или псевдоселектор (оба начинаются с одного :), предполагается, что они будут присоединены к другому селектору:
a:focusdiv:is(.active)button:active- … и т.д.
Если вы не присоединяете их непосредственно к селектору, логика анализа CSS добавляет неявный универсальный селектор (*) перед CSS функцией или псевдоселектором:
a :focus=>a *:focusdiv :is(.active)=>div *:is(.active)button :active=>button *:active
Теперь вы видите, что псевдоселекторы применяются к дочерним элементам, а не к самому элементу.
Если используется :has(), то селектор, помещённый внутрь него, уже применяется к дочерним элементам элемента, который вы выбираете. Это отличается от :is() и :not(), применяемых к самому элементу.
Значит, при добавлении неявных селекторов мы можем добавить неявный универсальный селектор перед вложенным селектором :not() и повторить его для вложенного селектора :has():
.card:has(*:not(img)) {}
.card:not(.card:has(img)) {}Разбиение селекторов на части
Если разбить селектор на части, будет проще понять, что делает каждая из них. Давайте разберём первый селектор, выделив часть внутри CSS функции:
.card:has() {} /* Выбор .card, содержащего несколько других селекторов */
*:not(img) {} /* выбор любого элемента, не являющегося изображением */
/* и */
.card:not() {} /* Выбор .card, если другие селекторы не соответствуют */
.card:has(img) {} /* Выбор .card, содержащего изображение */Теперь можно рассмотреть отдельные части и увидеть, что они делают.
Рассматривая изнутри наружу
В конечном итоге мы пытаемся выбрать .card, поэтому, глядя на селектор внутри CSS функции, можно упростить HTML до содержимого .card:
<!-- первая карта -->
<img />
<span />
<!-- вторая карта -->
<span />Теперь посмотрим на селекторы и сравним их с упрощённым HTML:
Первый вложенный селектор: *:not(img)
img— это изображение, поэтому*:not(img)не соответствуетspan— это не изображение, поэтому*:not(img)соответствует
Поскольку в нашей HTML структуре в обеих картах присутствует span, можно упростить задачу и переписать *:not(img) в span.
Полный первый селектор теперь становится .card:has(span): выбирает любую карту, содержащую элемент span. Оба HTML примера представляют собой карточки, содержащие элемент span, поэтому выбираются обе карты.
Второй вложенный селектор: .card:has(img)
- Первая карта содержит изображение, поэтому
.card:has(img)соответствует - Вторая карта не содержит изображения, поэтому
.card:has(img)не соответствует
Другими словами, если посмотреть на :has(), то для первой карты мы получим «true», а для второй — «false».
Если мы подставим это обратно в полный селектор, то получим .card:not(true) и .card:not(false).
:not() инвертирует логические значения, поэтому из двух HTML карт первая не будет выбрана, а вторая будет выбрана, потому что в ней нет изображения.
Заключение
Когда пытаешься понять, что делает CSS селектор, полезно разбить его на части и рассмотреть изнутри наружу. Так легче понять, какой HTML выбирает каждая часть селектора, а затем можно их объединить, чтобы получить полный HTML, которому соответствует селектор.
В данном случае мы рассмотрели разницу между :has(:not) и :not(:has), а также, как они выбирают элементы на основе наличия или отсутствия других элементов.
Если приведённое выше объяснение вам не помогло, то рекомендуем взглянуть на исследование Manuel Matuzović: :has(:not()) vs. :not(:has()).