:has(): селектор семейства

С начала времен (с точки зрения CSS) мы работали с каскадом в различных смыслах. Наши стили составляют «Каскадную таблицу стилей». И наши селекторы тоже каскадируются. Они могут пойти боком. В большинстве случаев они идут вниз. Но никогда вверх. В течение многих лет мы мечтали о «селекторе родителей». И вот оно наконец наступило! В форме псевдоселектора :has() .

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

Но это больше, чем «родительский» селектор. Это хороший способ продать это. Не столь привлекательным способом может быть селектор «условной среды». Но это звучит совсем по-другому. А как насчет «семейного» селектора?

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

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

Как использовать: есть

Так как же это выглядит? Рассмотрим следующий HTML-код с двумя родственными элементами класса everybody . Как бы вы выбрали тот, у которого есть потомок класса a-good-time ?

<div class="everybody">
  <div>
    <div class="a-good-time"></div>
  </div>
</div>

<div class="everybody"></div>

С помощью :has() вы можете сделать это с помощью следующего CSS.

.everybody:has(.a-good-time) {
  animation: party 21600s forwards;
}

При этом выбирается первый экземпляр .everybody и применяется animation .

В этом примере целью является элемент класса everybody . Условие — наличие потомка класса a-good-time .

<target>:has(<condition>) { <styles> }

Но вы можете пойти гораздо дальше, потому что :has() открывает много возможностей. Даже те, которые, вероятно, еще не обнаружены. Рассмотрим некоторые из них.

Выберите элементы figure , имеющие прямую figcaption . css figure:has(> figcaption) { ... } Выберите anchor , которые не имеют прямого потомка SVG css a:not(:has(> svg)) { ... } Выберите label , имеющие прямой input брат или сестра. Идем боком! css label:has(+ input) { … } Выберите article , в которых у потомка img нет alt текста css article:has(img:not([alt])) { … } Выберите documentElement , в котором присутствует некоторое состояние DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Выберите контейнер макета с нечетным числом дочерних элементов css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Выбрать все элементы в сетке, которые не наведены курсором css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Select контейнер, содержащий пользовательский элемент <todo-list> css main:has(todo-list) { ... } Выберите каждый соло a в абзаце, который имеет прямой родственный элемент hr css p:has(+ hr) a:only-child { … } Выберите article , в которой соблюдено несколько условий css article:has(>h1):has(>h2) { … } Смешайте это. Выберите article , в которой за заголовком следует подзаголовок css article:has(> h1 + h2) { … } Выберите :root при срабатывании интерактивных состояний css :root:has(a:hover) { … } Выберите абзац, который следует за figure , у которой нет figcaption css figure:not(:has(figcaption)) + p { … }

Какие интересные варианты использования :has() вы можете придумать? Самое интересное здесь то, что это побуждает вас сломать свою ментальную модель. Это заставляет задуматься: «Могу ли я подойти к этим стилям по-другому?».

Примеры

Давайте рассмотрим несколько примеров того, как мы можем его использовать.

Карты

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

<li class="card">
  <h2 class="card__title">
      <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
</li>

Что происходит, когда вы хотите представить какие-либо средства массовой информации? В этом дизайне карточку можно разделить на два столбца. Раньше вы могли создать новый класс для представления этого поведения, например card--with-media или card--two-columns . Эти имена классов не только трудно придумать, но и трудно поддерживать и запоминать.

С помощью :has() вы можете обнаружить, что на карте есть носитель, и выполнить соответствующие действия. Нет необходимости в именах классов модификаторов.

<li class="card">
  <h2 class="card__title">
    <a href="/article.html">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
</li>

И не нужно оставлять это там. Вы можете подойти к этому творчески. Как карточка с «рекомендованным» контентом может адаптироваться к макету? Этот CSS сделает избранную карточку полной шириной макета и поместит ее в начало сетки.

.card:has(.card__banner) {
  grid-row: 1;
  grid-column: 1 / -1;
  max-inline-size: 100%;
  grid-template-columns: 1fr 1fr;
  border-left-width: var(--size-4);
}

Что, если избранная карточка с баннером привлекает внимание?

<li class="card">
  <h2 class="card__title">
    <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
  <div class="card__banner"></div>
</li>

.card:has(.card__banner) {
  --color: var(--green-3-hsl);
  animation: wiggle 6s infinite;
}

Так много возможностей.

Формы

Как насчет форм? Они известны тем, что их сложно стилизовать. Одним из таких примеров является стилизация входных данных и их меток. Как мы, например, сигнализируем о том, что поле действительно? С :has() это становится намного проще. Мы можем подключиться к соответствующим псевдоклассам формы, например :valid и :invalid .

<div class="form-group">
  <label for="email" class="form-label">Email</label>
  <input
    required
    type="email"
    id="email"
    class="form-input"
    title="Enter valid email address"
    placeholder="Enter valid email address"
  />   
</div>
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() , чтобы показать и скрыть сообщение об ошибке для поля. Возьмите нашу группу полей «электронная почта» и добавьте в нее сообщение об ошибке.

<div class="form-group">
  <label for="email" class="form-label">
    Email
  </label>
  <div class="form-group__input">
    <input
      required
      type="email"
      id="email"
      class="form-input"
      title="Enter valid email address"
      placeholder="Enter valid email address"
    />   
    <div class="form-group__error">Enter a valid email address</div>
  </div>
</div>

По умолчанию вы скрываете сообщение об ошибке.

.form-group__error {
  display: none;
}

Но когда поле становится :invalid и не имеет фокуса, вы можете отобразить сообщение без необходимости использования дополнительных имен классов.

.form-group:has(:invalid:not(:focus)) .form-group__error {
  display: block;
}

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

Содержание

Мы коснулись этого в примерах кода. Но как можно использовать :has() в потоке документов? Например, это подбрасывает идеи о том, как мы могли бы стилизовать типографику в средствах массовой информации.

figure:not(:has(figcaption)) {
  float: left;
  margin: var(--size-fluid-2) var(--size-fluid-2) var(--size-fluid-2) 0;
}

figure:has(figcaption) {
  width: 100%;
  margin: var(--size-fluid-4) 0;
}

figure:has(figcaption) img {
  width: 100%;
}

Этот пример содержит цифры. Если у них нет figcaption , они плавают внутри содержимого. При наличии figcaption они занимают всю ширину и получают дополнительное поле.

Реакция на состояние

Как насчет того, чтобы ваши стили реагировали на какое-то состояние нашей разметки? Рассмотрим пример с «классической» выдвижной панелью навигации. Если у вас есть кнопка, которая переключает открытие навигации, она может использовать атрибут aria-expanded . JavaScript можно использовать для обновления соответствующих атрибутов. Если aria-expanded имеет true , используйте :has() , чтобы обнаружить это и обновить стили для скользящей навигации. JavaScript выполняет свою роль, а CSS может делать с этой информацией все, что захочет. Нет необходимости перетасовывать разметку или добавлять дополнительные имена классов и т. д. (Примечание: это не готовый к использованию пример).

:root:has([aria-expanded="true"]) {
    --open: 1;
}
body {
    transform: translateX(calc(var(--open, 0) * -200px));
}

Может ли :has помочь избежать ошибки пользователя?

Что общего у всех этих примеров? Помимо того, что они показывают способы использования :has() , ни один из них не требует изменения имен классов. Каждый из них добавил новый контент и обновил атрибут. Это большое преимущество :has() , поскольку оно может помочь уменьшить количество ошибок пользователя. С помощью :has() CSS может взять на себя ответственность за адаптацию к изменениям в DOM. Вам не нужно манипулировать именами классов в JavaScript, что снижает вероятность ошибок разработчика. Мы все сталкивались с ситуацией, когда мы опечатывали имя класса и вынуждены были сохранять его в поиске Object .

Это интересная мысль, и приведет ли она нас к более чистой разметке и меньшему количеству кода? Меньше JavaScript, поскольку мы не вносим столько изменений в JavaScript. Меньше HTML, поскольку вам больше не нужны такие классы, как card card--has-media и т. д.

Нестандартное мышление

Как упоминалось выше, :has() побуждает вас сломать ментальную модель. Это возможность попробовать разные вещи. Один из таких способов попытаться расширить границы — создать игровую механику только с помощью CSS. Например, вы можете создать пошаговую механику с помощью форм и CSS.

<div class="step">
  <label for="step--1">1</label>
  <input id="step--1" type="checkbox" />
</div>
<div class="step">
  <label for="step--2">2</label>
  <input id="step--2" type="checkbox" />
</div>
.step:has(:checked), .step:first-of-type:has(:checked) {
  --hue: 10;
  opacity: 0.2;
}


.step:has(:checked) + .step:not(.step:has(:checked)) {
  --hue: 210;
  opacity: 1;
}

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

А как насчет классической игры с проволокой? Механику проще создать с помощью :has() . Если проволока зависнет, игра окончена. Да, мы можем создать некоторые из этих игровых механик с помощью таких вещей, как одноуровневые комбинаторы ( + и ~ ). Но : :has() — это способ добиться тех же результатов без использования интересных «трюков» разметки. Обратите внимание: эту демонстрацию лучше всего просматривать в отдельной вкладке браузера.

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

:root:has(#start:checked):has(.game__success:hover, .screen--win:hover)
.screen--win {
  --display-win: 1;
}

Производительность и ограничения

Прежде чем мы начнем, что нельзя делать с :has() ? Существуют некоторые ограничения на :has() . Основные из них возникают из-за снижения производительности.

  • Вы не можете :has() a :has() . Но вы можете связать :has() . css :has(.a:has(.b)) { … }
  • Никакого использования псевдоэлементов внутри :has() css :has(::after) { … } :has(::first-letter) { … }
  • Ограничить использование :has() внутри псевдонимов, принимая только составные селекторы css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • Ограничить использование :has() после псевдоэлемента css ::part(foo):has(:focus) { … }
  • Использование :visited всегда будет ложным css :has(:visited) { … }

Чтобы узнать реальные показатели производительности, связанные с :has() , ознакомьтесь с этим Glitch . Благодарим Бёнву за то, что он поделился своими идеями и подробностями реализации.

Вот и все!

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

Все демоверсии доступны в этой коллекции CodePen .