Мощные селекторы: :has(), :is(), :where()
Урок про три функциональных псевдокласса: «родительский» селектор :has(), а также :is() и :where(), которые сокращают длинные списки — но по-разному влияют на специфичность.
Функциональные псевдоклассы принимают список селекторов в скобках:
:has()проверяет наличие потомка/состояния, а:is()и:where()сворачивают группы селекторов, различаясь только тем, как считается специфичность.
Зачем это на практике
Долгие годы CSS умел смотреть только «вниз и вправо» по дереву: нельзя было застилизовать родителя по содержимому. Длинные перечисления вроде h1, h2, h3, .title, .subtitle разрастались и дублировались. Эти три псевдокласса решают обе боли: :has() наконец даёт «восходящий» взгляд, а :is()/:where() делают селекторы короче и читаемее. Все три поддерживаются всеми актуальными браузерами.
:has() — «родительский» селектор
:has() выбирает элемент, внутри которого (или в связи с которым) есть то, что описано в скобках. Стилизуется при этом сам элемент слева, а не то, что в :has().
/* Карточка, у которой ВНУТРИ есть картинка */
.card:has(img) {
padding-top: 0;
}
/* label, рядом с которым лежит обязательное поле */
label:has(+ input:required)::after {
content: " *";
color: #dc2626;
}
/* Форма, в которой есть невалидное поле */
form:has(input:invalid) .submit {
opacity: 0.5;
}
Это открывает то, что раньше требовало JavaScript. Можно реагировать на состояние потомка: .gallery:has(:hover) приглушит остальные элементы, когда на один наведена мышь. Можно проверять количество: .list:has(li:nth-child(10)) сработает, когда в списке хотя бы 10 пунктов.
Селектор внутри :has() относительный — он рассматривается от элемента слева. div:has(> p) — это «div, у которого есть прямой потомок-абзац», а div:has(p) — «где-то внутри есть абзац».
:is() — короче длинные группы
:is(A, B, C) совпадает, если элемент подходит под любой из A, B, C. Главная польза — выносить общий контекст за скобки и не повторять его.
/* Было: дублируется .article перед каждым заголовком */
.article h1, .article h2, .article h3 { line-height: 1.2; }
/* Стало */
.article :is(h1, h2, h3) { line-height: 1.2; }
/* Особенно экономно при комбинаторном взрыве */
:is(header, main, footer) :is(a, button):hover {
text-decoration: underline;
}
Последнее правило без :is() пришлось бы расписывать в шесть отдельных селекторов. Ещё одно удобство: :is() «прощает» неизвестные селекторы в списке — один неподдерживаемый пункт не обрушит остальное правило (forgiving parsing).
:where() — то же, но без специфичности
:where() работает точно как :is(), но с одним критическим отличием: его собственная специфичность всегда равна нулю. Это делает его незаменимым для базовых стилей и библиотек, которые легко перекрыть.
/* Специфичность этого правила = 0, как у тега */
:where(.btn, .link) { color: inherit; }
/* Любой более конкретный селектор легко победит */
.btn.primary { color: white; } /* выиграет без !important */
Если бы вместо :where() стоял :is(), специфичность подскочила бы до уровня класса, и перекрыть базовое правило стало бы труднее.
Разная специфичность :is() и :where()
Это самый важный нюанс урока. У :where() специфичность всегда 0. У :is() специфичность равна самому «тяжёлому» селектору внутри скобок.
| Селектор | Специфичность |
:where(#id, .cls, p) | 0, 0, 0 |
:is(#id, .cls, p) | 1, 0, 0 (берётся от #id) |
:is(.cls, p) | 0, 1, 0 (берётся от .cls) |
:has(.cls) | 0, 1, 0 (как у самого тяжёлого внутри) |
Практический вывод: если положить тяжёлый селектор (особенно #id) внутрь :is(), он «заразит» всё правило высокой специфичностью, даже когда сработал лёгкий вариант из списка. Поэтому для предсказуемого каскада в дизайн-системах предпочитают :where().
Упрощение длинных списков
Реальная экономия видна на типографике статьи: общий контекст пишется один раз.
/* Сброс отступов у всех заголовков и медиа внутри материала */
.prose :where(h2, h3, h4, p, ul, ol, blockquote, figure) {
margin-block: 0.75em 0;
}
/* Состояния интерактивных элементов одним махом */
:is(a, button, [role="button"]):where(:hover, :focus-visible) {
outline: 2px solid currentColor;
}
Запятая внутри :is()/:where() — это «или», а сами псевдоклассы можно сцеплять, собирая компактные, но точные правила.
Как это работает под капотом
Браузер раскрывает :is() и :where() так, будто вы написали все комбинации вручную: .a :is(b, c) логически эквивалентно .a b, .a c. Разница лишь в шаге подсчёта специфичности — у :where() вклад скобок принудительно обнуляется. :has() устроен сложнее: чтобы понять, подходит ли элемент, движку нужно заглянуть в его поддерево (или к соседям), поэтому исторически он и появился позже — это «дорогой» по вычислениям селектор. Современные браузеры оптимизируют его инвалидацию, но злоупотреблять очень широкими :has() на больших деревьях не стоит.
Частые ошибки
- Думать, что :has() стилизует то, что в скобках. Оформляется элемент слева: в
.card:has(img)меняется карточка, а не картинка. - Класть #id в :is(). Тогда специфичность всего правила прыгает до уровня id и неожиданно перекрывает другие стили. Для «нейтральности» берите
:where(). - Ждать нулевую специфичность от :is(). Ноль даёт только
:where(); у:is()она равна самому тяжёлому пункту. - Забывать про относительность в :has().
:has(> p)(прямой потомок) и:has(p)(любой потомок) — это разные условия.
Итоги
:has()— долгожданный «родительский»/реляционный селектор: стилизует элемент слева по наличию потомка, соседа или их состояния.:is(A, B, C)и:where(A, B, C)сворачивают группы селекторов и выносят общий контекст за скобки.- Ключевое различие: специфичность
:where()всегда 0, а:is()равна самому тяжёлому селектору внутри. - Для базовых стилей и дизайн-систем берите
:where(), чтобы их легко перекрывать. - Не кладите
#idв:is(), если не хотите случайно поднять специфичность всего правила.