27 Модуль 6 Современный CSS 18 мин урок 2 из 25

:has(), :is(), :where() — селекторы, которые всё изменили

Родительский селектор, о котором просили двадцать лет. Плюс два способа группировать селекторы и вложенность прямо в CSS.

Что ты научишься делать

  • Научишься стилизовать родителя по содержимому — без единой строки JavaScript
  • Перестанешь плодить длинные перечисления селекторов через запятую
  • Поймёшь, чем :where() отличается от :is() и когда это важно

#Задача, которую раньше решали скриптом

Форма с ошибкой должна подсвечиваться целиком. Карточка с картинкой — верстаться в две колонки, без картинки — в одну. Меню, в котором есть активный пункт, — получать подчёркивание.

Во всех трёх случаях стиль зависит от содержимого. CSS умел смотреть только вниз: от родителя к детям. Поэтому такие вещи делали скриптом — навешивали класс на родителя.

:has() развернул направление. Теперь селектор может спросить: «а есть ли внутри меня вот это?»

CSSstyle.css
/* Поле, внутри которого есть сообщение об ошибке */.field:has(.error) {  border-color: #c8394a;  background: #fff5f6;}/* Карточка с картинкой — в две колонки */.card:has(img) {  grid-template-columns: 120px 1fr;}/* Меню, где есть активный пункт */.menu:has(.is-current) {  border-bottom: 2px solid currentColor;}

Интерактивный разбор доступен при включённом JavaScript.

#Как читать :has()

Правило простое: стилизуется то, что слева от :has(), а внутри скобок стоит условие.

.card:has(img) — «карточка, внутри которой есть картинка». Красится карточка, не картинка.

Внутри скобок работает обычный селектор потомка. Комбинаторы тоже работают, и они меняют смысл.

СелекторЧто означает
li:has(p)Пункт, внутри которого есть абзац на любой глубине
li:has(> p)Пункт, у которого абзац — прямой ребёнок
h2:has(+ p)Заголовок, сразу за которым идёт абзац
.card:not(:has(img))Карточка, внутри которой картинки нет
form:has(:invalid)Форма, в которой есть хотя бы одно неверное поле
Форма подсвечивается сама
В песочницу

#Счётные запросы

Приём, который выглядит как фокус: :has() умеет считать. Сетка может сама решать, сколько колонок ей нужно, по количеству карточек внутри.

Работает это так: :has(:nth-child(4)) — «внутри есть хотя бы четвёртый ребёнок», то есть детей не меньше четырёх.

CSSgallery.css
/* Один-два снимка — крупно, в одну колонку */.gallery { grid-template-columns: 1fr; }/* Появился третий — переходим на три колонки */.gallery:has(:nth-child(3)) {  grid-template-columns: repeat(3, 1fr);}/* Шесть и больше — мельчим и включаем перенос */.gallery:has(:nth-child(6)) {  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));}

#:is() — сокращение для длинных списков

Знакомая картина: одно и то же правило для шести селекторов, каждый со своим длинным началом.

Так не надо

.article h1,
.article h2,
.article h3,
.sidebar h1,
.sidebar h2,
.sidebar h3 {
  margin-block: 1.2em .4em;
}

Шесть строк ради одного правила. Добавится h4 — править в двух местах, и легко забыть одно.

Так надо

:is(.article, .sidebar) :is(h1, h2, h3) {
  margin-block: 1.2em .4em;
}

Одна строка. Добавить h4 — дописать одно слово. Читается почти как обычное предложение.

#:where() — то же самое, но с нулевым весом

:where() работает как :is(), но его специфичность всегда равна нулю. Совсем.

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

CSSbase.css
/* Вес 0-0-0: любой компонентный класс перекроет без борьбы */:where(.prose) :where(h2, h3) {  margin-block: 1.5em .5em;}/* И вот это спокойно побеждает, хотя селектор совсем простой */.card__title {  margin: 0;}
:is():where()
Что делаетГруппирует селекторыГруппирует селекторы
СпецифичностьКак у самого тяжёлого внутриВсегда ноль
Где уместенКомпоненты, где вес нуженСброс, база, библиотечные стили

#Вложенность прямо в CSS

Раньше за вложенностью шли в Sass. Теперь она есть в самом CSS: символ & означает «текущий селектор».

CSScard.css
.card {  padding: 20px;  border-radius: 14px;  & .card__title {    margin: 0 0 8px;  }  /* & перед псевдоклассом — это тот же .card */  &:hover {    box-shadow: 0 12px 30px -18px rgb(20 24 48 / .5);  }  /* Медиазапрос тоже можно вложить */  @media (min-width: 640px) {    padding: 28px;  }}

#Как это выглядит в реальном проекте

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

CSSproduct-card.css
@layer components {  .product {    display: grid;    gap: 12px;    padding: 16px;    /* Есть бейдж скидки — сдвигаем заголовок, чтобы не наезжал */    &:has(.product__badge) .product__title {      padding-inline-end: 64px;    }    /* Нет картинки — не резервируем под неё место */    &:not(:has(.product__media)) {      grid-template-rows: auto;    }    /* Товара нет в наличии — гасим всё, кроме кнопки «сообщить» */    &:has(.product__stock[data-empty]) :is(.product__price, .product__media) {      opacity: .45;    }  }}

#Когда НЕ надо

  • Заменять состояние скриптом наоборот. Если состояние всё равно приходит из JavaScript, честнее повесить класс, чем строить :has() на десять условий: класс читается за секунду, а длинный :has() — нет.
  • Гнаться за «без JS» любой ценой. Аккордеон на :has() и чекбоксах работает, но остаётся невидимым для программ чтения с экрана: у него нет ролей и состояний. Интерактив с семантикой — задача не для CSS.
  • Цеплять :has() к очень широким селекторам. * :has(img) заставляет браузер проверять каждый элемент дерева. На большой странице это заметно. Ограничивай левую часть конкретным классом.
  • Вкладывать глубже двух уровней. Вложенность экономит символы и тут же тратит их обратно на борьбу со специфичностью.

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

ВозможностьСостояние
:is(), :where()Во всех современных браузерах с 2021 года
:has()Safari первым, с 2022; Chrome и Firefox подтянулись к концу 2023
Вложенность с &Во всех современных браузерах с 2023 года

Где спотыкаются почти все

Ожидание, что покрасится то, что внутри скобок

Почему .card:has(img) читают как «картинка внутри карточки» и удивляются, что стиль ушёл на карточку.

Как надо Стилизуется всегда левая часть. Нужен стиль картинке — пиши .card:has(img) img.

:is() с идентификатором внутри

Почему :is(.card, #hero) получает вес идентификатора для обоих случаев. Правило начинает выигрывать там, где не должно.

Как надо Не смешивай уровни в одном :is(). Для базовых стилей бери :where().

Вложенность на четыре уровня

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

Как надо Два уровня максимум. Глубже — заводи отдельный класс с осмысленным именем.

Забыт пробел или комбинатор внутри :has()

Почему .card:has(.title) и .card:has(> .title) — разные вещи, а выглядят похоже.

Как надо Без комбинатора — любой потомок на любой глубине. Нужен прямой ребёнок — ставь >.

Родительский селектор :has()
Псевдокласс, выбирающий элемент по наличию подходящего потомка или соседа.
Группировка :is()
Сокращение для списка селекторов. Специфичность — по самому тяжёлому аргументу.
Нулевая специфичность :where()
То же, что :is(), но вес всегда ноль. Основа для базовых стилей.
Вложенность CSS CSS nesting
Запись правил внутри других правил через &, без препроцессора.

Проверь себя

  1. Какой элемент покрасит правило <code>.card:has(img) { background: teal; }</code>?

    Стилизуется всегда левая часть селектора. <code>:has()</code> — это условие, а не цель.

  2. Чему равна специфичность <code>:where(.card, #hero) .title</code>?

    <code>:where()</code> всегда весит ноль, независимо от содержимого. Остаётся только <code>.title</code> — точнее, один класс: 0-1-0.

  3. Чем <code>li:has(p)</code> отличается от <code>li:has(&gt; p)</code>?

    Без комбинатора внутри скобок подходит любой потомок. Со знаком <code>&gt;</code> — только непосредственный.

  4. Что произойдёт в браузере, который не знает <code>:has()</code>?

    Незнакомый псевдокласс обнуляет весь селектор. Страница остаётся в базовом виде — это безопасная деградация.

  5. В каких случаях <code>:has()</code> — хороший выбор?

    Верных вариантов несколько.

    Аккордеон — это интерактив с состоянием и ролями. CSS их не сообщает: программе чтения с экрана такой аккордеон останется непонятным.

Задание

Четыре состояния без JavaScript

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

  • карточку с бейджем .badge — с рамкой цвета #c8394a;
  • карточку без картинки — с фоном #f4f4f8;
  • заголовки внутри .catalog и .aside — одним правилом через :is();
  • базовый отступ абзаца — через :where(), чтобы класс его легко перебивал.
Открыть в песочнице
    Подсказка 1

    Условие пишется в скобках, а красится то, что слева: .card:has(.badge) { border-color: #c8394a; }

    Подсказка 2

    Отсутствие проверяется через :not(:has(...)) — «карточка, внутри которой нет img».

    Подсказка 3

    :where(p) { margin-bottom: 12px; } — вес нуль, поэтому .tight { margin: 0 } победит без борьбы.

    Показать решение
    HTMLindex.html
    <section class="catalog">  <h2>Каталог</h2>  <article class="card">    <span class="badge">-30%</span>    <img src="" alt="Кофе" width="60" height="40">    <h3>Раф</h3>    <p>Вкусно</p>  </article>  <article class="card">    <h3>Эспрессо</h3>    <p class="tight">Без картинки</p>  </article></section><aside class="aside">  <h2>Сбоку</h2></aside>
    CSSstyle.css
    .card:has(.badge) {  border-color: #c8394a;}.card:not(:has(img)) {  background: #f4f4f8;}:is(.catalog, .aside) h2 {  font-size: 20px;  margin: 0 0 10px;}:where(p) {  margin-bottom: 12px;}.card {  display: grid;  gap: 6px;  padding: 12px;  margin-bottom: 10px;  border: 2px solid #ddd;  border-radius: 10px;}.badge { font-weight: 700; color: #c8394a; }img { background: linear-gradient(135deg, #7c5cff, #b06cff); border-radius: 6px; }.tight { margin: 0; }

    Проверь себя