:has() — родительский селектор

Вступление

За годы работы с HTML, CSS и немного JavaScript я замечаю одну приятную закономерность: ВЭБ развивается. Причем развивается очень быстро. Для некоторых действий раньше приходилось писать много кода для построения сеток, но пришли Flex и Grid. Для стилизации некоторых блоков приходилось добавлять лишние классы, но пришли nth- селекторы. Но чего-то не хватало. Всё равно встречались моменты когда необходимо было добавлять лишние классы и приходилось с этим работать. И вот приходит декабрь 2023 год, затем январь 2024 года и 90% браузеров поддерживают новый псевдокласс :has().

видео

:has() — родительский селектор

Первый пример использования :has()

Что это такое и с чем его едят?

Рассмотрим следующий пример:

HTML

<label for="email" class="email-label">
  <span class="visually-hidden">Введите email</span>
  <input type="email" name="email" id="email" placeholder="Email" class="email-input">
</label>
<label for="email-1" class="email-label">
  <span class="visually-hidden">Введите email</span>
  <input type="email" name="email" id="email-1" placeholder="Email" class="email-input" value="email">
</label>
<label for="email-2" class="email-label">
  <span class="visually-hidden">Введите email</span>
  <input type="email" name="email" id="email-2" placeholder="Email" class="email-input" disabled>
</label>

CSS

.visually-hidden {
  position: absolute !important;
  clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
  clip: rect(1px, 1px, 1px, 1px);
  padding: 0 !important;
  border: 0 !important;
  height: 1px !important;
  width: 1px !important;
  overflow: hidden;
}

.email-label {
  position: relative;
}

.email-label::after {
  content: '';
  position: absolute;
  right: 5px;
  top: 50%;
  transform: translateY(-50%);
  width: 35px;
  height: 35px;
  mask-image: url(../img/email.svg);
  mask-repeat: no-repeat;
  mask-position: center;
  mask-size: contain;
  background-color: currentColor;
}

.email-input {
  display: block;
  margin: 0;
  padding: 4px;
  padding-right: 40px;
  font-size: 30px;
  line-height: 1;
  border: 3px solid #000000;
  border-radius: 4px;
}

.email-input:hover {
  border: 3px solid#0039a6;
}

.email-label:hover {
  color: #0039a6;
}

.email-input:focus {
  outline-offset: 3px;
}

.email-input:invalid {
  border-color: #d52b1e;
}

.email-input:disabled {
  border-color: #747474;
}

Вроде бы всё предусмотрено, но есть неприятные моменты.

Скриншот работы кода. Первый input в спокойном состоянии, второй в состоянии :invlid, третий в состоянии :disabled
У input в состояниях :invlid и :disabled иконки "tmail" черного цвета, а обводка input красного и серого.

Если при наведении на элемент можно с помощью чистого CSS изменить цвет иконки, то в состояниях :invalid и :disabled нужно было динамически добавлять класс для label. Но с помощью с помощью псевдокласса :has() этот вопрос решается чистым CSS:

CSS

.email-label:has(.email-input:invalid) {
  color: #d52b1e;
}

.email-label:has(.email-input:disabled) {
  color: #747474;
}

Не правда ли здорово? Теперь нет необходимости писать JavaScript для отслеживания состояний input.

Скриншот работы кода. Первый input в состоянии наведения с синей рамкой и иконкой, второй инпут с красной рамкой и иконкой, третий с серой рамкой и иконкой.
Теперь все иконки одного цвета с рамкой

Второй пример.

Это не единственный пример использовать данный псевдокласс. Допустим, пишем какую-то длинную статью с множеством разделов.

HTML

<div class="container-1">
  <section>
    <h2>Заголовок</h2>
    <p>Какой-то текст</p>
  </section>

  <section>
    <h2>Заголовок</h2>
    <img src="#" alt="Описание картинки">
  </section>
</div>

CSS

.container-1 {
  display: grid;
  grid-template-columns: 1fr 1fr;
}

.container-1 h2 {
  margin-top: 0;
}

А по макету если после заголовков идёт тег <p>, то этому заголовку нужно обнулить нижний отступ. Опять же нас выручит псевдокласс :has()

CSS

h2:has(+ p) {
  margin-bottom: 0;
}
Скриншот работы кода. У первого заголовка подсвечивается только контентная часть, а у второго контентная часть и margin.
У заголовка под которой тег <p> нет нижнего отступа.

Этот псевдокласс просто манна небесная. Позволяет творить чудеса.

Третий пример :has() + :is().

Браузеры добавляют ссылкам подчёркивание, но иногда это подчёркивание мешает.

Три ссылки с незагруженной картинкой, текст подчеркнут.
У ссылок с битой картинкой текст с подчеркиванием.

Обычно подчёркивание оставляют, для того чтобы было понятно что это ссылка, но если нужно в некоторых блоках его убрать? Например в <header> и <footer>, где списки ссылок и подчеркивание можно убрать. И так понятно что это ссылка. При помощи одного только :has() нам придётся добавить два селектора с одним правилом:

HTML

<header>
  <a href="#">
    <img src="#" alt="Описание картинки">
  </a>
</header>
<main>
  <a href="#">
    <img src="#" alt="Описание картинки">
  </a>
</main>
<footer>
  <a href="#">
    <img src="#" alt="Описание картинки">
  </a>
</footer>

CSS

header a:has(> img),
footer a:has(> img) {
  text-decoration: none;
}
Три ссылки с незагруженной картинкой, текст подчеркнут только у средней.
Теперь текст ссылок в header и footer без подчеркивания.

Можно ли написать это правило одним селектором? Да, и поможет нам псевдокласс :is():

CSS

:is(header, footer) a:has(> img) {
  text-decoration: none;
}

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

Предыдущий элемент.

HTML

<div class="block">1</div>
<div class="block center">center</div>
<div class="block">2</div>

CSS

.block {
  padding: 5px;
}

*:has(+ .center) {
  background-color: #ff6347;
}
Скриншот трёх блоков
Элемент перед блоком с классом .center окрашен в томатный цвет.

Специфичность.

Важно не забывать про специфичность селекторов. И в данном случае всё просто: рассмотренные псевдоклассы не имеют никакого веса. Однако вес создают теги, классы и другие псевдоклассы, которые и нужно учитывать.

Важное дополнение.

  • Не стоит создавать селекторы с этим псевдоклассом и псевдоэлементами, судя по спецификации работать не будет.
  • На MDN сказано что такие селекторы не будут работать в JavaScript.

Итоги

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

Скриншот с сервиса caniuse о поддержке браузерами псевдокласса :has()
На момент написания статьи поддержка псевдокласса :has() браузерами составляет 91.96%.

Если для России можно использовать такие подходы, то для ряда восточных/азиатских стран лучше не рисковать. Но нужно смотреть какие именно стили добавляются с помощью этих свойств. Из моих примеров с инпутами или подчеркивания ссылок такой приём можно использовать везде. Так как вёрстка в целом не ломается.

Объявление

Нравится фронтенд — попробуйте самые подробные интерактивные тренажёры от HTML Academy бесплатно. Подойдут и действующим разработчикам, чтобы подтянуть навыки вёрстки.