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

:has() – реляционный CSS-псевдокласс из Selectors Level 4: выбирает элемент по наличию потомка, соседа или состояния внутри. Долгожданный «родительский селектор» работает во всех браузерах с декабря 2023 (Firefox 121).

3 минуты чтения

Запрос на «родительский селектор» – возможность стилизовать элемент в зависимости от его содержимого – разработчики повторяли больше десяти лет: CSS умел выбирать потомков по родителю, но не наоборот. Псевдокласс :has() из спецификации Selectors Level 4 закрыл эту потребность и оказался заметно шире. Формально это реляционный псевдокласс: он проверяет условие относительно элемента – наличие потомка, следующего соседа, состояние поля формы – и применяет стили, только когда условие выполнено. В паре с псевдоклассом отрицания :not() он закрывает сценарии, ради которых раньше подключали JavaScript и навешивали классы-флаги.

Как это работает

Синтаксис – селектор:has(аргумент). Браузер «якорит» проверку на элементе слева: section:has(img) выберет секцию, внутри которой на любой глубине есть изображение; сама картинка при этом не стилизуется. Аргумент – список относительных селекторов через запятую, причём список forgiving («прощающий»): невалидный селектор в нём игнорируется и не ломает правило.

Комбинаторы внутри аргумента меняют смысл проверки:

  • :has(img) – потомок на любой глубине;
  • :has(> img) – только прямой потомок;
  • h1:has(+ h2) – «предыдущий сосед»: выбирается заголовок, сразу за которым идёт h2;
  • :has(video, audio) – логическое ИЛИ; цепочка :has(video):has(audio) – И.

Типовые применения в одном блоке кода:

/* Родитель по ребёнку: карточка с изображением и без */
.card:has(.card__image) { display: flex; gap: 1rem; }
.card:not(:has(.card__image)) { border-top: 3px solid #7c93e9; }

/* Подсветка группы полей формы при ошибке */
.form-group:has(:invalid) { border-color: #d33; }

/* Quantity query: от 5 элементов – плитка мельче */
.wrapper:has(.item:nth-last-child(n + 5)) { --item-size: 120px; }

Последний пример – так называемый quantity query: связка с :nth-last-child() меняет стили контейнера в зависимости от количества дочерних элементов. До :has() это делалось только скриптом.

:has() и классы-флаги из JavaScript – в чём разница

Стилизация родителя: :has() против JS-классов
Критерий:has()JS + классы-флаги
Логикаживёт в CSS, одно правилоразнесена между скриптом и стилями
Разметкачистая, без модификаторовслужебные классы вида has-image
Изменения DOMбраузер пересчитывает стили самнужен MutationObserver или повторный вызов
Отказоустойчивостьработает без JavaScriptломается, если скрипт упал

Ограничения зафиксированы в спецификации: вкладывать :has() внутрь :has() нельзя, а псевдоэлементы вроде ::before невалидны и внутри аргумента, и как якорь. В браузере без поддержки селектор с :has() инвалидирует всё правило (вне forgiving-обёрток :is() и :where()), поэтому для прогрессивного улучшения используют проверку @supports selector(:has(a)).

Специфичность :has() равна специфичности самого «сильного» селектора в аргументе – сам псевдокласс ничего не добавляет. div:has(#promo) весит как селектор с ID. Как это влияет на каскад – в статье о специфичности CSS.

Зачем это нужно

Практическая ценность – меньше JavaScript и чище разметка. До :has() реакция родителя на содержимое требовала скрипта, который следил за DOM и переключал классы; теперь то же самое делает браузер. Типовые задачи:

  • навигация – подсветить пункт меню, содержащий активную ссылку: li:has(.current);
  • карточки – разные макеты для карточки с изображением и без, без модификаторов в разметке;
  • формы – стилизация label и целых групп по состояниям :checked, :invalid, :focus вложенных полей;
  • глобальные состояния – реакция страницы на виджет: :root:has([aria-expanded="true"]), удобно в связке с aria-expanded.

Цена вопроса – производительность: браузеру приходится обходить поддерево якорного элемента при каждой перепроверке. Рекомендации MDN и команды Chrome: не якорить :has() на body, :root или универсальном селекторе * и сужать область проверки комбинаторами > и +.container:has(> img) вместо .container:has(img).

Поддержка :has() браузерами
БраузерВерсияДата
Safari15.4март 2022, первый браузер с поддержкой
Chrome, Edge105август 2022
Firefox121декабрь 2023
BaselineNewly availableдекабрь 2023
BaselineWidely availableориентировочно июнь 2026, через 30 месяцев

Глобальная поддержка по caniuse – около 92,7% на июль 2026. Не поддерживают :has() только IE и Opera Mini. Если проект обязан работать в устаревших браузерах, базовые стили пишут без :has(), а улучшения заворачивают в блок @supports selector(:has(a)) – тогда правило с неподдерживаемым селектором не инвалидирует остальной CSS.

Пример

Классическая задача форм: показать дополнительное поле, когда пользователь выбрал в списке вариант «Другое».

.other-field { display: none; }

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

Раньше здесь понадобился бы обработчик change, который навешивает класс на форму. Одно CSS-правило заменяет обработчик события и продолжает работать, даже если JavaScript отключён или не успел загрузиться. По тому же принципу подсвечивают подпись активного поля – label:has(+ input:focus).

В практике с 1С-Битрикс :has() экономит время на типовых компонентах: они генерируют фиксированную разметку, и чтобы добавить класс-модификатор, шаблон компонента пришлось бы копировать и править. С :has() обёртку стилизуют по содержимому – например, прячут блок фильтра, в котором не осталось ни одного пункта, – одним правилом в CSS, без правки template.php.

Частые вопросы

Правда ли, что :has() – это «родительский селектор» в CSS?

Да, это главный сценарий: section:has(img) стилизует родителя по наличию потомка. Но :has() шире – это реляционный псевдокласс: он проверяет и следующего соседа (h1:has(+ h2)), и состояние полей формы, и количество дочерних элементов. Родительский селектор – лишь частный случай.

Как выбрать элемент, за которым идёт определённый сосед?

Через комбинатор соседа внутри аргумента: h1:has(+ h2) выберет заголовок, сразу за которым идёт h2. До появления :has() CSS умел стилизовать только следующий элемент по предыдущему, обратное направление было недоступно.

Можно ли вкладывать :has() внутрь :has()?

Нет, спецификация Selectors Level 4 прямо запрещает вложенность :has() в :has(). Псевдоэлементы ::before и ::after внутри аргумента тоже невалидны. Сложные условия собирают цепочкой: .a:has(.b):has(.c) – логическое И.

Какая специфичность у псевдокласса :has()?

Сам :has() специфичности не добавляет: берётся специфичность самого специфичного селектора в его аргументе, как у :is() и :not(). Например, div:has(#promo) весит как селектор с ID – это стоит учитывать, чтобы не получить трудноперебиваемые правила.

Можно ли использовать :has() в продакшене в 2026 году?

Да. Safari поддерживает его с версии 15.4 (март 2022), Chrome и Edge – со 105 (август 2022), Firefox – со 121 (декабрь 2023). Статус Baseline Newly available присвоен в декабре 2023, глобальная поддержка по caniuse – около 92,7%. Для старых браузеров есть проверка @supports selector(:has(a)).

Влияет ли :has() на производительность страницы?

Может влиять: браузер обходит поддерево якорного элемента при каждой перепроверке стилей. Рекомендации MDN и команды Chrome – не якорить :has() на body, :root или универсальном селекторе * и сужать проверку комбинаторами > и +, например .container:has(> img) вместо .container:has(img).

Как выбрать элементы, внутри которых чего-то нет?

Комбинацией с :not(): селектор figure:not(:has(figcaption)) выберет все figure без подписи. Это приём «отрицание по потомку», который до :has() средствами CSS был невозможен – требовался JavaScript.

Материалы по теме

Валентина Меланина

Нужна консультация?

Разберу ваш сайт и покажу точки роста

Если хотите понять, как этот термин применить к вашему проекту — начнём с аудита.