Декодирование CSS селекторов: :has(:not) и :not(:has)

Источник: «Decoding CSS Selectors: :has(:not) vs :not(:has)»
При комбинировании CSS функций вложенность функций имеет значение. В статье мы рассмотрим разницу между :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), но добавляют разную условную логику.

Если рассмотреть приведённые выше селекторы, есть три вещи, помогающие лучше понять, что они делают:

  1. Добавьте неявные селекторы, которые были пропущены.
  2. Разбейте селекторы на части
  3. Рассмотрите селектор изнутри наружу

Добавление неявных селекторов

Когда есть CSS функция или псевдоселектор (оба начинаются с одного :), предполагается, что они будут присоединены к другому селектору:

Если вы не присоединяете их непосредственно к селектору, логика анализа CSS добавляет неявный универсальный селектор (*) перед CSS функцией или псевдоселектором:

Теперь вы видите, что псевдоселекторы применяются к дочерним элементам, а не к самому элементу.

Если используется :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)

Поскольку в нашей HTML структуре в обеих картах присутствует span, можно упростить задачу и переписать *:not(img) в span.

Полный первый селектор теперь становится .card:has(span): выбирает любую карту, содержащую элемент span. Оба HTML примера представляют собой карточки, содержащие элемент span, поэтому выбираются обе карты.

Второй вложенный селектор: .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()).

Комментарии


Дополнительные материалы

Предыдущая Статья

Теперь CSS функция attr() поддерживает типы

Следующая Статья

Четыре распространенные ошибки Vite в Laravel