Декодирование 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:focus
div:is(.active)
button:active
- … и т.д.
Если вы не присоединяете их непосредственно к селектору, логика анализа CSS добавляет неявный универсальный селектор (*
) перед CSS функцией или псевдоселектором:
a :focus
=>a *:focus
div :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()).