Эта статья — перевод оригинальной статьи Ahmad Shadeed "CSS Parent Selector"

Также я веду телеграм канал “Frontend по-флотски”, где рассказываю про интересные вещи из мира разработки интерфейсов.

Вступление

Вы когда-нибудь задумывались о селекторе CSS, где вы проверяете, существует ли конкретный элемент внутри родителя? Например, если у компонента карточки есть миниатюра, нам нужно добавить к нему display: flex. Это было невозможно в CSS, но теперь у нас будет новый селектор CSS :has, который поможет нам выбрать родителя определенного элемента и многое другое.

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

Проблема

Возможность стилизовать родительский элемент отсутствует. Мы должны создавать классы CSS и переключать их в зависимости от того, что нам нужно.

Рассмотрим следующий базовый пример.

У нас есть компонент карточки в двух вариациях: 1) С изображением 2) Без изображения. В CSS мы могли бы сделать что-то вроде этого:

/* A card with an image */
.card {
    display: flex;
    align-items: center;
    gap: 1rem;
}

/* A card without an image */
.card--plain {
    display: block;
    border-top: 3px solid #7c93e9;
}
<!-- Card with an image -->
<div class="card">
    <div class="card__image">
        <img src="awameh.jpg" alt="">
    </div>
    <div class="card__content">
        <!-- Card content here -->
    </div>
</div>

<!-- Card without an image -->
<div class="card card--plain">
    <div class="card__content">
        <!-- Card content here -->
    </div>
</div>

Как вы видели выше, мы создали класс специально для карточки без изображения, поскольку нам не нужен display: flex на родительском элементе. Вопрос в том, можем ли мы это сделать в CSS, без второго класса?

Ну, вот где CSS :has приходит на помощь. Это может помочь нам проверить, есть ли у элемента .card изображение .card__image или нет.

Например, мы можем проверить, есть ли у карточки изображение, и если да, то нам нужно применить flexbox.

.card:has(.card__image) {
    display: flex;
    align-items: center;
}

Знакомство с :has селектором

Согласно спецификации CSS, селектор :has проверяет, содержит ли родитель хотя бы один конкретный элемент или выполняется ли условие, например, если инпут сфокусирован.

Вернемся к предыдущему фрагменту кода.

.card:has(.card__image) { }

Мы проверяем, содержит ли родительский элемент .card дочерний элемент .card__image. Рассмотрим следующий рисунок:

Проще говоря, приведенный выше CSS эквивалентен следующему: "Есть ли в карточке элемент .card__image?"

Разве это не восхитительно? В CSS есть логика!

Селектор :has не только про родителя

Речь идет не только о проверке того, содержит ли родитель дочерний элемент, но мы также можем проверить, следует ли за элементом, например, <p>. Рассмотрим следующее:

.card h2:has(+ p) { }

Это проверяет, следует ли <h2> непосредственно за <p>. Или мы можем использовать его с элементом формы, например, чтобы проверить, есть ли сфокусированный инпут.

form:has(input:focused) {
    background-color: lightgrey;
}

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

На момент написания статьи CSS :has работал в Safari 15.4 и в Chrome Canary. Следите за поддержкой на Can I use.

Можем ли мы использовать это внутри @supports?

Да, можем!

@supports selector(:has(*)) {
    /* do something */
}

Хватит теории, давайте перейдем к примерам использования!

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

Заголовок раздела

Когда я работаю над заголовком раздела, у меня в основном будет два варианта: один только с заголовком, а другой содержит и заголовок, и якорную ссылку.

В зависимости от того, есть ли ссылка или нет, я хочу оформить его по-разному.

<section>
    <div class="section-header">
        <h2>Latest articles</h2>
        <a href="/articles/>See all</a>
    </div>
</section>

Обратите внимание, что я использовал :has(> a), который выберет только прямую дочернюю ссылку.

.section-header {
  display: flex;
  justify-content: space-between;
}

/* If there is a link, add the following */
.section-header:has(> a) {
  align-items: center;
  border-bottom: 1px solid;
  padding-bottom: 0.5rem;
}

Демо

Компонент карточки, Пример 1

Вернемся немного назад к примеру с исходной картой. У нас есть два варианта, один с изображением, а другой без него.

.card:has(.card__image) {
    display: flex;
    align-items: center;
}

Мы даже можем проверить, нет ли на .card изображения, и применить определенные стили. В нашем случае это border-top.

.card:not(:has(.card__image)) {
    border-top: 3px solid #7c93e9;
}

Без :has нам пришлось бы иметь два класса, чтобы сделать это.

.card--default {
    display: flex;
    align-items: center;
}

.card--plain {
    border-top: 3px solid #7c93e9;
}

Демо

Компонент карточки, Пример 2

В этом примере у нас есть два варианта набора действий у каждой карточки: одна карточка с одним элементом (ссылка), а другая с несколькими действиями (сохранить, поделиться и т. д.).

Когда действия карточки имеют две разные обёртки для действий, мы хотим активировать display: flex следующим образом (пожалуйста, не обращайте внимания на приведенную ниже разметку, она предназначена исключительно для демонстрационных целей!).

<div class="card">
    <div class="card__thumb><img src="cool.jpg"/></div>
    <div class="card__content">
        <div class="card__actions">
            <div class="start">
                <a href="#">Like</a>
                <a href="#">Save</a>
            </div>
            <div class="end">
                <a href="#">More</a>
            </div>
        </div>
    </div>
</div>
.card__actions:has(.start, .end) {
    display: flex;
    justify-content: space-between;
}

Вот что нам придётся делать без :has.

.card--with-actions .card__actions {
    display: flex;
    justify-content: space-between;
}

Демо

Компонент карточки, Пример 3

Вам когда-нибудь приходилось сбрасывать border-radius для компонента в зависимости от того, есть ли изображение или нет? Это идеальное использование CSS :has.

Рассмотрим следующий рисунок. Когда изображение удалено, border-radius верхнего левого и правого углов равен нулю, что выглядит странно.

.card:not(:has(img)) .card__content {
    border-top-left-radius: 12px;
    border-top-right-radius: 12px;
}

.card img {
    border-top-left-radius: 12px;
    border-top-right-radius: 12px;
}

.card__content {
    border-bottom-left-radius: 12px;
    border-bottom-right-radius: 12px;
}

Намного лучше!

Вот что нам придётся написать без использования :has.

.card--plain .card__content {
    border-top-left-radius: 12px;
    border-top-right-radius: 12px;
}

Демо

Компонент фильтрации

В этом примере у нас есть компонент с несколькими параметрами. Когда ни один из них не отмечен, кнопки сброса нет. Однако, когда хотя бы один отмечен, нам нужно показать кнопку сброса.

Мы можем легко сделать это с помощью CSS :has.

.btn-reset {
    display: none;
}

.multiselect:has(input:checked) .btn-reset {
    display: block;
}

Мы вообще не можем сделать это в CSS без :has. Это один из сценариев, когда мы откажемся от Javascript, если :has будет поддерживаться в современных браузерах.

Демо

Показать или скрыть элементы формы по условию

Возможно, нам потребуется показать конкретное поле формы на основе предыдущего ответа или выбора. В этом примере нам нужно показать поле «other», если пользователь выбрал «other» в меню выбора.

С помощью CSS :has мы можем проверить, выбрано ли в меню значение "other", и показать поле «other» на основе этого.

.other-field {
    display: block;
}

form:has(option[value="other"]:checked) .other-field {
    display: block;
}

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

Демо

Элемент навигации с подменю

В этом примере у нас есть элемент навигации с подменю, которое появляется при наведении или фокусе.

Что мы хотим сделать, так это скрыть стрелку в зависимости от того, есть ли меню или нет. Мы можем легко сделать это с помощью CSS :has. Идея состоит в том, чтобы проверить, содержится ли <li> в <ul>

/* Check if the <li> has a <ul>. Yes? show the arrow. */
li:has(ul) > a:after {
    content: "";
    /* arrow styling */
}

Без CSS :has у нас, вероятно, будет класс для <li> с подменю. Что-то вроде следующего:

.nav-item--with-sub > a:after {
    content: "";
    /* arrow styling */
}

Демо

Обёртка для шапки

При создании компонента заголовка мы можем быть уверены в том, хотим ли мы, чтобы заголовок занимал всю ширину страницы или был в обёртке.

В любом случае нам нужно применить flexbox для распределения элементов заголовка определенным образом. Если .wrapper есть, мы применим к нему стили. Если нет, то применим их непосредственно к элементу .site-header.

<header class="site-header">
    <div class="wrapper">
        <!-- Header content -->
    </div>
</header>
.site-header:not(:has(.wrapper)) {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-inline: 1rem;
}

/* If it has a wrapper */
.site-header .wrapper {
    display: flex;
    justify-content: space-between;
    align-items: center;
    max-width: 1000px;
    margin-inline: auto;
    padding-inline: 1rem;
}

Демо

Акцент на алертах

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

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

С помощью CSS :has мы можем проверить, есть ли предупреждение в элементе .main, и если да, мы можем добавить следующие стили в заголовок.

.main:has(.alert) .header {
    border-top: 2px solid red;
    background-color: #fff4f4;
}

Демо

Смена темы

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

html {
    --color-1: #9e7ec8;
    --color-2: #f1dceb;
}

И когда мы выбираем другой вариант из списка, вот что происходит в CSS. В зависимости от выбранной опции переменные CSS будут изменены.

html:has(option[value="blueish"]:checked) {
    --color-1: #9e7ec8;
    --color-2: #f1dceb;
}

Демо

Стилизация сгенерированного HTML

В некоторых случаях у нас нет никакого контроля над HTML. Например, в теле статьи. Система управления контентом (CMS) может генерировать элементы неожиданным образом, или автор может встроить видео или что-то в этом роде.

Предположим, что мы хотим выбрать тот <h3>, за которым не следует абзац, и увеличить интервал под ним.

.article-body h3:not(:has(+ p)) {
    margin-bottom: 1.5rem;
}

Или вам нужно выбрать <iframe>, за которым следует <h3> и что-то сделать. Такие ситуации не могут быть обработаны без CSS :has!

.article-body h3:has(+ p) {
    /* do something */
}

Кнопка с иконкой

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

.button:has(.c-icon) {
    display: inline-flex;
    justify-content: center;
    align-items: center;
}

Демо

Несколько кнопок

В дизайн-системе нам часто требуется группа кнопок. Если у нас есть более двух кнопок, последняя должна отображаться на дальней противоположной стороне.

Для этого мы можем использовать количественные запросы. Следующий CSS проверит, равно ли количество кнопок трём или больше, и если да, последний элемент будет сдвинут вправо с помощью margin-left: auto.

.btn-group {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
  
.btn-group:has(.button:nth-last-child(n + 3)) .button:last-child {
    margin-left: auto;
}

Демо

Информационные модули

Я получил этот пример из дизайна pinterest. Когда инпут имеет ошибку, мы хотим, чтобы заголовок изменился и указывал на это.

.module:has(.input-error) .headline {
    color: #ca3131;
}

Изменить сетку в зависимости от количества элементов

С сеткой CSS мы можем использовать функцию minmax() для создания отзывчивых элементов сетки с автоматическим размером. Однако этого может быть недостаточно. Мы также хотим изменить сетку в зависимости от количества элементов.

Рассмотрим следующий рисунок.

.wrapper {
    --item-size: 200px;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(var(--item-size), 1fr));
    gap: 1rem;
}

Когда у нас будет 5 элементов, последний будет перенесен в новую строку.

Мы можем преодолеть это, проверив, содержит ли .wrapper 5 или более элементов. Опять же, здесь используется концепция количественных запросов.

.wrapper:has(.item:nth-last-child(n + 5)) {
    --item-size: 120px;
}

Figure и Figcaption

В этом примере у нас есть HTML <figure>. Если есть <figcaption> стиль должен немного отличаться:

  • Добавляем белый фон

  • Немного паддингов

  • Уменьшаем border-radius картинки

figure:has(figcaption) {
    padding: 0.5rem;
    background-color: #fff;
    box-shadow: 0 3px 10px 0 rgba(#000, 0.1);
    border-radius: 3px;
}

Заключение

Мне не терпится увидеть, что вы создадите с помощью CSS :has. Сценарии использования в этой статье — лишь малая часть! Я уверен, что мы обнаружим много полезных применений по пути.

Как говорится, самое подходящее время для изучения CSS. Я очень, очень взволнован тем, что будет дальше. Большое спасибо за чтение!

Комментарии (1)


  1. BlackStar1991
    24.04.2022 09:07

    зачем расписывать технологию у которой поддержка только в Safary? Даже читать всё это не имеет смысла, пока она не будет хотя бы в черновиках Chrome-подобных браузеров озвучена