Псевдокласс CSS :has()

Spread the love

Псевдокласс CSS :has() выбирает элементы, которые содержат другие элементы, соответствующие селектору, переданному в его аргументы. Его часто называют «родительским селектором» из-за его способности выбирать родительский элемент на основе содержащихся в нем дочерних элементов и применять стили к родителю.

/* Select the .card element when it 
   contains a <figure> immediately
   followed by a paragraph. */
.card:has(figure + p) {
  flex-direction: row;
}

В этом примере выбирается элемент с классом .card, если он содержит элемент <figure> , за которым сразу следует элемент <p>:

<article class="card">
  <figure></figure>
  <p></p>
</article>

Это невероятно полезно при написания стилей для компонентов, которые могут содержать или не содержать определенные элементы внутри, например сетку карточек, где элемент карточки всегда имеет абзац, но может не иметь сопровождающего его изображения.

Image or no image? That is the question.

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

:has() определен в спецификации Selectors Level 4, где он описывается как «реляционный псевдокласс» (“the relational pseudo-class” ) из-за его способности сопоставлять селекторы на основе отношения элемента к другим элементам.

Базовое использование

Следующий HTML содержит два элемента <button> . Один из них имеет элемент SVG.

<!-- Plain button -->
<button>Add</button>

<!-- Button with SVG icon -->
<button>
  <svg></svg>
  Add
</button>

Предположим, вы хотите применить стили только к <button>, внутри которой находится элемент <svg> .

:has() идеально подходит для работы:

button:has(svg) {
  /* Styles */
}

Селектор :has() дает нам возможность различать кнопку, у которой есть потомок<svg> , и кнопку, у которой его нет.

Синтаксис

:has( <unforgiving-relative-selector-list> )

unforgiving список селекторов представляет собой список элементов, разделенных запятыми, и которые оцениваются вместе на основе их отношения к родительскому элементу.

article:has(ol, ul) {
  /* Соответствует элементу <article>, который содержит либо
      упорядоченный или неупорядоченный список. */
}

Чуть позже мы более подробно разберем «unforgiving» природу списка аргументов.

Специфичность (Specificity)

Один из наиболее интересных аспектов :has() заключается в том, что его специфичность определяется наиболее «конкретным» (specific ) элементом в списке аргументов. Скажем, у нас есть следующие правила стиля:

article:has(.some-class, #id, img) {
  background: #000;
}

article .some-class {
  background: #fff;
}

У нас есть два правила, каждое из которых выбирает элемент <article> 
для изменения его фона. Какой фон у этого HTML?

<article>
  <div class="some-class"></div>
</article>

Вы можете подумать, что будет белый фон (#fff), потому что он появляется позже в каскаде (cascade). Но поскольку список аргументов для :has() включает другие селекторы, мы должны рассмотреть наиболее специфичный (specific) из них, чтобы определить реальную специфику этого первого правила. В данном случае это будет #id.

Давайте сравним их:

  • article:has(.some-class, #id, img) генерирует оценку (1,0,1)
  • article.some-class генерирует оценку (0,1,1)

Первое правило побеждает! Элемент получает черный (#000) фон.

Вопрос на понимание!

Как вы думаете, какой цвет побеждает в следующем примере?

article:has(h1, .title) a { 
  color: red; 
}

article h1 a {
  color: green;
}

Ответ.

/* Specificity: (0,1,2) */
article:has(h1, .title) a {
  color: red; /* 🏆 Winner! */
}

/* Specificity: (0,0,3) */
article h1 a {
  color: green;
}

:has() является «unforgiving» селектором

Первый проект спецификации представил :has() как «forgiving selector”»:

:has( <forgiving-relative-selector-list> )

Идея в том, что список может содержать недопустимый селектор и игнорировать его.

/* Example: Не использовать! */
article:has(h2, ul, ::-scoobydoo) { }

::-scoobydoo — полностью выдуманный недействительный псевдоэлемент, которого не существует. Если бы :has() было «forgiving», этот фиктивный селектор был бы просто проигнорирован, в то время как остальные аргументы анализировались бы как обычно.

Но позже из-за конфликта с поведением jQuery ( conflict with jQuery’s behavior) авторы спецификации решили определить :has() как non-forgiving селектор. В результате :is() и :where() являются единственными прощающими относительными селекторами группы.

Это означает, что :has() ведет себя как составной селектор (compound selector). Согласно спецификациям CSS (CSS specifications), по устаревшим причинам общее поведение составного селектора заключается в том, что если какой-либо селектор в списке недействителен, весь список селекторов недействителен, что приводит к отбрасыванию всего набора правил.

/* Это ничего не делает, потому что `::-scoobydoo`
    недопустимый селектор */
a, a::-scoobydoo {
  color: green;
}

То же самое относится и к :has(). Любой недопустимый селектор в его списке аргументов сделает недействительным все остальное в списке. Итак, тот пример, который мы рассматривали ранее:

/* Пример: Не использовать! */
article:has(h2, ul, ::-scoobydoo) { }

… вообще ничего не будет делать. Все три селектора в списке аргументов недействительны из-за этого недопустимого селектора ::scoobydoo . Все или ничего.

Но для этого есть обходной путь. Помните, :is() и :where() снисходительны, даже если :has() – нет. Это означает, что мы можем вложить любой из этих селекторов в :has() , чтобы получить более снисходительное поведение:

p:has(:where(a, a::scoobydoo)) {
  color: green;
}

Итак, если вы когда-нибудь захотите, чтобы :has() работал как «forgiving» селектор, попробуйте вложить :is() или :where() внутрь него.

Список аргументов принимает сложные селекторы

Сложный селектор (complex selector) содержит один или несколько составных селекторов (например, a.fancy-link) и комбинаторов (например, >, +, ~). Список аргументов для:has() принимает эти комплексные селекторы и может использоваться для определения отношений между несколькими элементами.

<relative-selector> = <combinator>? <complex-selector>

Вот пример относительного селектора, который содержит сложный селектор с дочерним комбинатором (>). Он выбирает элементы с классом .icon, которые являются прямыми дочерними элементами ссылок, имеющих класс .fancy-link и находящихся в состоянии :focus:

a.fancy-link:focus > .icon {
  /* Styles */
}

Подобные вещи можно использовать непосредственно в списке аргументов :has():

p:has(a.fancy-link:focus > .icon) {
  /* Styles */
}

Но вместо выбора элементов .icon, которые являются прямыми дочерними элементами ссылок .fancy-class, находящихся в :focus, мы оформляем абзацы, имеющие сфокусированные ссылки .fancy, прямыми дочерними элементами, имеющими класс .icon.

Как правило, :has не поддерживает псевдоселекторы.

Обычно :has() не поддерживает другие псевдоэлементы в своих аргументах, потому что так указано в спецификации:

Примечание. Псевдоэлементы, как правило, исключаются из :has(), потому что многие из них существуют условно, в зависимости от стиля их предков, поэтому если разрешить их запрашивать с помощью :has(), это приведет к возникновению циклов.

Но в действительности, есть несколько «псевдоэлементов has-allowed», разрешенных в списке аргументов :has(). Спецификация предлагает следующий пример, показывающий, как :not() используется вместе с :has():

/* Matches any <section> element that contains 
   anything that’s not a heading element. */
section:has(:not(h1, h2, h3, h4, h5, h6))

Джей Томпкинс предлагает еще один пример (Jhey Tompkins), показывающий, как :has() можно использовать для оформления форм на основе различных состояний ввода, таких как :valid, :invalid и :placeholder-show:

label {
  color: var(--color);
}
input {
  border: 4px solid var(--color);
}

.form-group:has(:invalid) {
  --color: var(--invalid);
}

.form-group:has(:focus) {
  --color: var(--focus);
}

.form-group:has(:valid) {
  --color: var(--valid);
}

.form-group:has(:placeholder-shown) {
  --color: var(--blur);
}

:has() не может вкладываться сам в себя, но поддерживает цепочку

К сожалению, :has() не поддерживает вложенности а-ля :has() в :has():

/* Nesting is a no-go */
.header-group:has(.subtitle:has(h2)) {
  /* Invalid! */
}

Это создало бы бесконечный цикл, в котором специфичность оценивается внутри другой оценки. Однако он позволяет вам связывать аргументы:

h2,
.subtitle {
  margin-block-end: 1.5rem;
}

/* Reduce spacing on header because the subtitle will handle it */
.header-group:has(h2):has(.subtitle) h2 {
  margin-block-end: 0.2rem;
}

Цепочка работает как логическая операция И в том смысле, что оба условия должны совпадать, чтобы правило стиля вступило в силу. Предположим, у вас есть список .news-статей и содержащиеся в нем статьи распределены по категориям. Возможно, вы захотите применить к списку определенные стили, но только если он содержит статьи с .breaking-news и статьи с .featured-news, но оставить все без изменений, если только одна из статей соответствует этим классам или не соответствует ни одной.

Вы можете связать два объявления :has() для этого условного стиля:

.news-list:has(.featured-news):has(.breaking-news) { 
  /* Styles */ 
}

Этот пример относится только к контейнеру .news-list. Если бы мы хотели сопоставить какой-либо старый родительский элемент, который имеет классы .featured-news и .breaking-news в статьях, мы могли бы вообще исключить .news-list:

:has(.featured-news):has(.breaking-news) { 
  /* Styles */ 
}

:has больше, чем «родительский» селектор

Джей Томпкинс называет его «семейным» селектором (Jhey Thompkins), что может быть более подходящим описанием, особенно когда речь идет о последнем примере, который мы рассмотрели. Давайте посмотрим на это снова:

.header-group:has(h2):has(.subtitle) h2 {
  margin-block-end: 0.2rem;
}

Мы не просто выбираем элемент с классом .header-group, который содержит элемент h2
Это полномочия по выбору родителей, обычно приписываемые :has(). Мы выбираем элемент:

  • с классом .subtitle
  • который дочерний элемент .header-group
  • и содержит элемент <h2> внутри него.

Является ли  <h2> прямым потомком .header-group? Нет, это больше похоже на внука.

Сочетание :has() с другими реляционными псевдоселекторами

Вы можете комбинировать :has() с другими функциональными селекторами псевдоклассов, такими как :where():not() и :is().

Объединение :has() и :is()

Например, вы можете проверить, имеет ли какой-либо из заголовков HTML хотя бы один элемент <a>  в качестве потомка:

:is(h1, h2, h3, h4, h5, h6):has(a) {
  color: blue;
}

/* is equivalent to: */
h1:has(a),
h2:has(a),
h3:has(a),
h4:has(a),
h5:has(a),
h6:has(a) {
  color: blue;
}

Вы также можете передать :is() в качестве аргумента :has(). Представьте, если бы мы изменили последний пример так, чтобы был выбран любой уровень заголовка, содержащий дочерний элемент <a> или любой дочерний элемент с классом .link:

:is(h1, h2, h3, h4, h5, h6):has(:is(a, .link)) {
  color: blue;
}

Сочетание :has() и :not()

Мы также можем использовать :has() с селектором :not()! Допустим, вы хотите добавить рамку к элементу .card, если он не содержит потомков элемента  <img> . Конечно:

.card:not(:has(img)) {
  border: 1px solid var(--my-amazing-color);
}

Это условие проверяет, есть ли в .card какое-либо изображение, а затем говорит:

Эй, если вы не найдете ни одного изображения, пожалуйста, примените эти стили.

Хотите сойти с ума? Давайте выберем любой элемент .post для изображений, у которых отсутствует альтернативный текст (alt):

.post:has(img:not([alt])) {
  /* Styles */
}

Видишь, что мы здесь сделали? На этот раз :not() находится в :has(). Что означает:

Эй, если вы найдете какие-либо сообщения, содержащие изображение без альтернативного текста (alt), примените эти стили.

Это можно использовать для поиска изображений, в которых отсутствует атрибут alt:

Вот еще один пример, который я позаимствовал из видео Эрика Мейера (Eric Meyer’s). Допустим, вы хотите выбрать любой <div>, в котором нет ничего, кроме элементов <img>.

div:not(:has(:not(img))) {
  /* Styles */
}

Вот как это можно прочитать:

Если вы нашли <div> и в нем нет ничего, кроме одного или нескольких изображений, творите свое волшебство!

Ситуации, когда порядок имеет значение

Обратите внимание, как изменение порядка наших селекторов меняет то, что они выбирают. Мы говорили о unforgiving  характере списка аргументов: has(), но он еще менее unforgiving  (прощающий) в рассмотренных нами примерах, в которых :has() сочетается с другими реляционными псевдоселекторами.

Давайте посмотрим на пример:

article:not(:has(img)) {
  /* Styles */
}

Это соответствует всем элементам  <article>, которые не содержат изображений. Теперь давайте поменяем местами так, чтобы :has() стоял перед :not():

article:has(:not(img)) {
  /* Styles */
}

Теперь мы сопоставляем любой элемент <article>, который содержит что-либо, если в нем нет изображений. У <article> должен быть соответствующий потомок, и этот потомок может быть чем угодно, но не изображением.

Случаи использования

Разделители хлебных крошек (Breadcrumb)

Хлебные крошки – это удобный способ показать, на какой странице в данный момент находится пользователь и какое место эта страница занимает на карте сайта. Например, если вы находитесь на странице «О нас», вы можете отобразить список, содержащий элемент со ссылкой на домашнюю страницу и элемент, который просто указывает на текущую страницу:

<ol class="breadcrumb">
  <li class="breadcrumb-item"><a href="/">Home</a></li>
  <li class="breadcrumb-item current">About</li>
</ol>

Это классно. Но что, если мы хотим отобразить это как горизонтальный список и скрыть номера списка? Достаточно просто с CSS:

ol {
  display: flex;
  list-style: none;
}

Примечание. Установка list-style: none запрещает Safari идентифицировать элемент как список.

Хорошо, но теперь у нас осталось два элемента списка, которые пересекаются друг с другом. Мы могли бы добавить зазор между ними, так как мы используем Flexbox:

ol {
  display: flex;
  gap: .5rem;
  list-style: none;
}

Это, безусловно, помогает. Но мы можем провести более сильное различие между двумя элементами, добавив между ними разделитель. Ничего страшного:

.breadcrumb-item::after {
  content: "/";
}

Но стоп! Нам не нужен разделитель после элемента .current, потому что он всегда последний в списке и после него ничего нет. Здесь в игру вступает :has() . Мы можем найти любого дочернего элемента с классом .current, используя последующий дочерний комбинатор (~), чтобы обнаружить его:

.breadcrumb-item:has(~ .current)::after {
  content: "/";
}

Пример реализации.

Форма валидации без JavaScript

Как мы узнали ранее, :has() не принимает псевдоэлементы, но позволяет нам использовать псевдоклассы. Мы можем использовать это как легкую форму проверки, с которой мы обычно сталкиваемся при помощи JavaScript.

Допустим, у нас есть форма подписки на рассылку новостей, которая запрашивает адрес электронной почты:

<form>  
  <label for="email-input">Add your pretty email:</label>
  <input id="email-input" type="email" required>
</form>

Электронная почта является обязательным полем в этой форме. Иначе нечего предъявить! Возможно, мы можем добавить красную рамку для ввода, если пользователь вводит неверный адрес электронной почты:

form:has(input:invalid) {
  border: 1px solid red;
}

Попробуй. Попробуйте ввести неверный адрес электронной почты, а затем вкладку или щелкните поле «Пароль».

Стилизация завершенных строк в списке to-do

Пробовали ли вы стилизовать метку label при проверке ввода?

Структура HTML кода может выглядеть так:

<form>
  <input id="example-checkbox" type="checkbox">
  <label for="example-checkbox">We need to target this when input is checked</label>
</form>

Хотя лучше поместить label перед элементом ввода или обернуть ее вокруг элемента ввода для удобства доступа, вы должны поместить метку после ввода, чтобы иметь возможность выбрать label на основе атрибута проверки ввода.

Используя следующий родственный комбинатор (+), вы можете оформить метку следующим образом:

/* When the input is checked, 
  style the label */
input:checked + label {
  color: green;
}

Давайте изменим разметку и создадим имплецитную метку, вложив в нее input:

<form>  
  <label>
    <input type="checkbox">
    We need to target this when input is checked
  </label>
</form>

Раньше не было возможности выбрать эту метку при проверке input. Но теперь, когда у нас есть селектор :has(), у нас есть все возможности:

/* If a label has a checked input,
  style that label */
label:has(input:checked) {
  color: green;
}

Теперь вернемся к идеальной разметке, в которой label стоит перед input:

<form> 
  <label for="example-checkbox">We need to target this when input is checked</label>
  <input id="example-checkbox" type="checkbox">
</form>

Вы по-прежнему можете использовать :has() в качестве предыдущего селектора для выбора и оформления явной label, сохраняя при этом более доступную разметку:

/* If a label has a checked input 
   that is it's next sibling, style 
   the label */
label:has(+ input:checked) {
  color: green;
}

Умная кнопка «Добавить в корзину»

Что произойдет, если мы применим :has() к корневому элементу или телу страницы?

:root:has( /* Any condition */ ) {
  /* Styles */
}

:root – это самый высокий уровень документа, который управляет всем, что находится под ним, верно? Если что-то происходит где-то далеко внизу в дереве DOM, вы можете обнаружить это и соответствующим образом стилизовать другую ветвь DOM.

Допустим, вы запускаете сайт электронной коммерции и хотите оформить кнопку «Добавить в корзину», когда товар добавляется в корзину. Это довольно распространено, верно? Такие сайты, как Amazon, постоянно делают это, чтобы пользователь знал, что товар успешно находится в его корзине.

Представьте, что это структура нашего HTML. Кнопка «Добавить в корзину» содержится в элементе <header>, а товар — в элементе  <main>.

A diagram that identifies where the product and button items are locted in the DOM tree.

Супер упрощенный пример разметки может выглядеть так:

<body>
  <header>
    <button class="cart-button">Add to cart</button>
  </header>
  <main> 
    <ul>
      <li class="p-item">Product</li>
      <li class="p-item is-selected">Product</li>
      <li class="p-item">Product</li>
    </ul>
  </main>
</body>

В CSS мы можем проверить, есть ли у :has() какой-либо потомок с классами .p-item и .is-selected. Как только это условие выполняется, можно выбрать кнопку .cart:

body:has(.p-item.is-selected) .cart-button {
  background-color: green;
}

Изменение цветовых тем

Темный режим, светлый режим, высококонтрастный режим. Предоставление пользователям возможности настраивать цветовую тему сайта может стать хорошим улучшением UX.

Допустим, где-то глубоко в документе у вас есть меню  для выбора пользователями цветовой темы:

<select>
  <option value="light">Light</option>
  <option value="dark">Dark</option>
  <option value="high-contrast">High-contrast</option>
</select>

Мы можем использовать :has() для элемента и проверить выбранный элемент в меню <select> . Таким образом, если <option>  содержит определенное значение, мы можем обновить пользовательские свойства CSS с разными значениями цвета, чтобы изменить текущую цветовую тему:

body:has(option[value="dark"]:checked) {
  --primary-color: #e43;
  --surface-color: #1b1b1b;
  --text-color: #eee;
}

Опять же, что-то происходит (пользователь <select> s <option>) где-то внизу дерева DOM, и мы наблюдаем за изменениями на самом высоком уровне дерева (<body>) и обновляем стили (через ccustom свойства) соответственно.

Стиль элемента на основе количества дочерних элементов

Пример от Брамуса Ван Дамма (Bramus Van Damme). :has() может применять стили в зависимости от количества дочерних элементов в родительском контейнере.

Представьте, что у вас есть двухколоночный макет. Если количество элементов в сетке равно os — 3, 5, 7, 9 и т. д. — тогда вы застряли с пустым местом в сетке после последнего элемента.

Adjusting a two-column layout with an odd number of children where the first child spans the first row.

Было бы лучше, если бы первый элемент в сетке мог занимать два столбца в первой строке, чтобы этого не произошло. А для этого вам нужно проверить, является ли элемент :last-child в сетке дочерним элементом с нечетным номером:

/* If the last item in a grid is an odd-numbered child */
.grid-item:last-child:nth-child(odd) {
  /* Styles */
}

Его можно передать в список аргументов :has(), чтобы мы могли стилизовать :first-child сетки так, чтобы он занимал всю первую строку сетки, когда :last-child является нечетным числом:

.grid:has(> .grid-item:last-child:nth-child(odd)) .grid-item:first-child {
  grid-column: 1 / -1;
}

Поддержка браузера

Эти данные поддержки браузера получены от Caniuse, в котором есть более подробная информация. Число указывает, что браузер поддерживает эту функцию в этой версии и выше.

Desktop

ChromeFirefoxIEEdgeSafari
105NoNo10515.4

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
108No10815.4

Проверка поддержки

At-правило @supports поддерживает :has(), что означает, что мы можем проверить, поддерживает ли браузер :has, и применить стили условно на основе результата:

@supports(figure(:has(figcaption))) {
  /* Supported! */
}

ule supports :has() meaning we can check whether a browser supports it and apply styles conditionally based on the result:

@supports(figure(:has(figcaption))) {
  /* Supported! */
}

Перевод оригинальной статьи: :has()

Была ли вам полезна эта статья?
[0 / 0]

Spread the love
Подписаться
Уведомление о
guest
0 Комментарий
Inline Feedbacks
View all comments