Селекторы в CSS позволяют отобрать нужные элементы по тегу, атрибутам или положению в документе. В этой статье мы рассмотрим три относительно новых селектора, связанных со вложенностью элементов — :is()
, :where()
и :has()
.
Это так называемые псевдоклассы — селекторы, которые описывают текущее состояние элемента. Самый известный псевдокласс, пожалуй, это :hover
, который применяется к элементу, находящемуся под курсором. Другие популярные псевдоклассы:
:visited
— посещенные ссылки;:target
— элемент, id которого совпадает со значением хеша в урле;:first-child
— элементы, являющиеся первыми потомками у своих родителей;:nth-child
— позволяет отобрать потомков по их порядковому номеру у родителя;:emtpy
— элементы без потомков;:checked
— «включенные» чекбоксы и радио-кнопки;:blank
— пустые поля ввода;:enabled
— доступные для изменения поля ввода;:disabled
— недоступные для изменения поля ввода;:required
— поля, помеченные атрибутомrequired
;:valid
— правильно заполненные поля ввода (в соответствии с указанными ограничениями);:invalid
— неправильно заполненные поля ввода;:playing
— воспроизводящееся видео или аудио.
И вот недавно у нас появились еще три псевдокласса, давайте разбираться, зачем они нужны.
:is
Изначально этот селектор назывался :matches()
или :any()
, но в стандарт вошел под именем :is()
.
Чаще всего нам требуется применить стили больше, чем к одному элементу.
/* черный по умолчанию */
p {
color: #000;
}
/* серый внутри <article>, <section> или <aside> */
article p,
section p,
aside p {
color: #444;
}
В этом примере, цвет текста элемента p
будет по умолчанию черным. Но если p
находится внутри article
, section
или aside
, текст будет серым.
Этот код выглядит довольно просто, но чем сложнее наши страницы, тем сложнее селекторы, которые мы используем. Достаточно синтаксической ошибки, чтобы все стили сломались.
Препроцессоры CSS предлагают использовать механизм вложенности (который мы так ждем в нативном CSS):
article, section, aside {
p {
color: #444;
}
}
Этот фрагмент идентичен предыдущему, но при этом короче, что позволяет предотвратить ошибки.
Однако для работы с препроцессорами вам требуется отдельный инструмент сборки, а также изучение самого препроцессора, что усложняет разработку. Кроме того, слишком глубокая вложенность — это тоже проблема, она затрудняет понимание кода.
Селектор :is()
это нативное решение для подобных ситуаций, и оно уже полностью поддерживается всеми современными браузерами:
:is(article, section, aside) p {
color: #444;
}
В одном селекторе можно объединять несколько :is()
.
article section:not(:first-child):is(.primary, .secondary) :is(h1, h2, p) {
color: green;
}
Этот стиль применится ко всем элементам h1
, h2
и p
, которые находятся внутри section
с классом .primary
или .secondary
. При этом section
находится внутри article
и не является первым потомком своего родителя.
Альтернативный код без :is()
выглядит вот так:
article section.primary:not(:first-child) h1,
article section.primary:not(:first-child) h2,
article section.primary:not(:first-child) p,
article section.secondary:not(:first-child) h1,
article section.secondary:not(:first-child) h2,
article section.secondary:not(:first-child) p {
color: green;
}
Важно: селектор :is()
не работает с псевдоэлементами (::before
, ::after
). Вот такой код не будет работать:
div:is(::before, ::after) {
display: block;
content: '';
width: 1em;
height: 1em;
color: blue;
}
:where
Селектор :where
работает точно так же, как :is
, и тоже поддерживается во всех современных браузерах.
:where(article, section, aside) p {
color: #444;
}
Разница между ними — в специфичности. Специфичность — это алгоритм для определения, какой селектор из нескольких важнее.
Например, article p
более специфичен, чем просто p
, поэтому при возникновении конфликтов будут применяться именно его стили:
article p { color: #444; }
p { color: #000; }
Больше о специфичности и каскаде в CSS: Введение в CSS Cascade Layers
Специфичность селектора :is()
равна специфичности самого важного селектора в скобках. Специфичность :where()
всегда равна нулю.
Посмотрим на пример:
article p {
color: black;
}
:is(article, section, aside) p {
color: red;
}
:where(article, section, aside) p {
color: blue;
}
Текст абзаца будет красным:
See the Pen Using the :is selector by SitePoint (@SitePoint) on CodePen.
Селектор :is()
имеет такую же специфичность, как article p
, но идет после него, поэтому его стили и срабатывают.
А вот селектор :where()
менее специфичен, поэтому чтобы текст стал синим, нужно удалить два первых селектора.
Очевидно, что большинство проектов будут использовать :is()
чаще, чем :where()
, однако не нужно недооценивать его значимость. Нулевая специфичность отлично подходит для сброса стилей (CSS reset).
Возьмем вот такой пример сброса:
/* CSS reset */
h2 {
margin-block-start: 1em;
}
article :first-child {
margin-block-start: 0;
}
Для заголовка установлен верхний маргин, но если он окажется первым потомком элемента article
, то отступ будет обнулен.
Теперь попытка установить кастомный отступ для h2
не будет иметь эффекта, так как селектор article :first-child
более специфичен:
h2 {
margin-block-start: 2em;
}
Можно увеличить специфичность селектора:
article h2:first-child {
margin-block-start: 2em;
}
Это сработает, но сильно усложнит последующий код. Кроме того, сразу не очевидно, зачем потребовалась такая громоздкая конструкция.
Можно добавить !important
(но не нужно!).
Но лучше всего упростить сброс стилей, лишив селекторы ненужной специфичности:
:where(h2) {
margin-block-start: 1em;
}
:where(article :first-child) {
margin-block-start: 0;
}
Теперь для переопределения сработает даже самый простой селектор:
h2 {
margin-block-start: 2em;
}
:has
Селектор :has()
имеет такой же синтаксис, как :is()
и :where()
, но работает в другую сторону. Он отбирает элементы, которые имеют определенных потомков — селекторы потомков указываются в скобках.
a:has(img, section) {
border: 2px solid blue;
}
Этот стиль будет применен к ссылке, внутри которой находится img
или section
.
Это, вероятно, самое прекрасное, что появилось в CSS за многие годы! Наконец-то у нас появилась возможность отбирать родительские элементы, а не дочерние!
«Родительский» селектор — одна из самых желанных фич в CSS, но он создает для браузеров большие проблемы с производительностью, поэтому для его внедрения требуется много времени. Если коротко:
- Стиль применяется к элементу, когда он отображается на странице. Поэтому при добавлении потомков необходимо перерисовать весь родительский элемент.
- Добавление, удаление или изменение элементов из JavaScript может повлиять на стиль всей страницы, вплоть до
body
.
Если браузерам удастся решить проблемы с производительностью, :has()
откроет перед нами возможности, которые ранее были недоступны без использования JavaScript.
Например, можно задать стиль для fieldset
и кнопки отправки формы, если какое-либо из полей ввода невалидно:
fieldset:has(:required:invalid) {
border: 3px solid red;
}
fieldset:has(:required:invalid) + button[type='submit'] {
opacity: 0.2;
cursor: not-allowed;
}
Можно добавить стили для элемента навигации, если у него есть вложенное меню:
nav li:has(ol, ul) a::after {
display: inlne-block;
content: ">";
}
Или даже использовать эту возможность для дебаггинга, например, выделяя элементы figure
, внутри которых нет изображения:
/* Где мое изображение?! */
figure:not(:has(img)) {
border: 3px solid red;
}
Прежде чем бежать переписывать весь свой CSS, обратите внимание, что поддержка :has()
еще очень слабая. Он доступен в Safari 15.4+ и Chrome 101+ с флагом.
Заключение
Псевдоклассы :is()
и :where()
существенно упрощают синтаксис CSS, сокращая потребность во вложенных селекторах и препроцессорах.
Псевдокласс :has()
обладает колоссальным потенциалом и точно станет суперпопулярным, когда его начнут поддерживать основные браузеры.
0 комментариев